Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e54881d
Enhance adapter validation by adding URL security checks in AdapterPr…
gaya3-zipstack Sep 10, 2025
0963f8d
Add Whitelist to sample.env
gaya3-zipstack Sep 10, 2025
21320be
Update backend/adapter_processor_v2/views.py
gaya3-zipstack Sep 11, 2025
4307bf6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 11, 2025
c5e6359
Add url validation for PUT API
gaya3-zipstack Sep 11, 2025
1d90cba
Merge branch 'fix/url_safety_net' of https://github.com/Zipstack/unst…
gaya3-zipstack Sep 11, 2025
5196608
Update backend/adapter_processor_v2/views.py
gaya3-zipstack Sep 11, 2025
7fbcc33
Keep exception handling uniform across CREATE and UPDATE APIS
gaya3-zipstack Sep 11, 2025
9d6ce54
Keep exception handling uniform across CREATE and UPDATE APIS
gaya3-zipstack Sep 11, 2025
fb1ea01
Merge branch 'fix/url_safety_net' of https://github.com/Zipstack/unst…
gaya3-zipstack Sep 11, 2025
6a7bb7c
Fix indentation errors
gaya3-zipstack Sep 11, 2025
08c5044
Fix logical condition
gaya3-zipstack Sep 11, 2025
db3acff
Update backend/adapter_processor_v2/views.py
gaya3-zipstack Sep 11, 2025
a756ddb
Reafctorinf for SONAR issue fixes
gaya3-zipstack Sep 12, 2025
9310c68
Resolve conflict
gaya3-zipstack Sep 12, 2025
ea273a1
Fix SONAR issues - unused variable definition
gaya3-zipstack Sep 12, 2025
9fbb82d
Merge branch 'main' into fix/url_safety_net
gaya3-zipstack Sep 15, 2025
7588621
pass validate_urls=true
gaya3-zipstack Sep 15, 2025
6985184
Merge branch 'fix/url_safety_net' of https://github.com/Zipstack/unst…
gaya3-zipstack Sep 15, 2025
082f56f
Update backend/adapter_processor_v2/views.py
gaya3-zipstack Sep 16, 2025
d78e5fb
Revert changes in previous commit for coderabbit issue as it is not a…
gaya3-zipstack Sep 16, 2025
c217694
Merge branch 'main' into fix/url_safety_net
gaya3-zipstack Sep 16, 2025
534a577
Add validation of endpoints for API/ETL and postprocessor hook
gaya3-zipstack Sep 17, 2025
0b74f6f
Merge branch 'fix/url_safety_net' of https://github.com/Zipstack/unst…
gaya3-zipstack Sep 17, 2025
ac1d5c5
Update backend/notification_v2/provider/webhook/webhook.py
gaya3-zipstack Sep 18, 2025
cd129cf
URL validation in variable replacement
gaya3-zipstack Sep 22, 2025
2f5213f
Add sample env variable
gaya3-zipstack Sep 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions backend/adapter_processor_v2/adapter_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
from cryptography.fernet import Fernet
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from platform_settings_v2.platform_auth_service import PlatformAuthenticationService
from tenant_account_v2.organization_member_service import OrganizationMemberService
from platform_settings_v2.platform_auth_service import (
PlatformAuthenticationService,
)
from rest_framework.exceptions import ValidationError
from tenant_account_v2.organization_member_service import (
OrganizationMemberService,
)

from adapter_processor_v2.constants import AdapterKeys, AllowedDomains
from adapter_processor_v2.exceptions import (
Expand All @@ -27,7 +32,9 @@
logger = logging.getLogger(__name__)

try:
from plugins.subscription.time_trials.subscription_adapter import add_unstract_key
from plugins.subscription.time_trials.subscription_adapter import (
add_unstract_key,
)
except ImportError:
add_unstract_key = None

Expand Down Expand Up @@ -92,8 +99,6 @@ def get_adapter_data_with_key(adapter_id: str, key_value: str) -> Any:
def test_adapter(adapter_id: str, adapter_metadata: dict[str, Any]) -> bool:
logger.info(f"Testing adapter: {adapter_id}")
try:
adapter_class = Adapterkit().get_adapter_class_by_adapter_id(adapter_id)

if adapter_metadata.pop(AdapterKeys.ADAPTER_TYPE) == AdapterKeys.X2TEXT:
if (
adapter_metadata.get(AdapterKeys.PLATFORM_PROVIDED_UNSTRACT_KEY)
Expand All @@ -107,7 +112,17 @@ def test_adapter(adapter_id: str, adapter_metadata: dict[str, Any]) -> bool:
platform_key.key
)

adapter_instance = adapter_class(adapter_metadata)
# Validate URLs for this adapter configuration
try:
adapter_instance = AdapterProcessor.validate_adapter_urls(
adapter_id, adapter_metadata
)
except Exception as e:
# Format error message similar to test adapter API
adapter_name = adapter_metadata.get(AdapterKeys.ADAPTER_NAME, "adapter")
error_detail = f"Error testing '{adapter_name}'. {e!s}"
raise ValidationError(error_detail) from e
# adapter_instance = adapter_class(adapter_metadata)
test_result: bool = adapter_instance.test_connection()
return test_result
except SdkError as e:
Expand All @@ -130,6 +145,36 @@ def update_adapter_metadata(adapter_metadata_b: Any, **kwargs) -> Any:
return adapter_metadata_b
return adapter_metadata_b

@staticmethod
def validate_adapter_urls(adapter_id: str, adapter_metadata: dict) -> Adapter:
"""Validate URLs for an adapter configuration without full connection test.

This method only validates URLs for security (SSRF protection) without
attempting actual network connections.

Args:
adapter_id: The adapter ID (e.g., "postgres|70ab6cc2...")
adapter_metadata: The adapter configuration metadata

Returns:
Adapter: The adapter instance if validation passes

Raises:
AdapterError: If URL validation fails due to security violations
"""
try:
# Get the adapter class
adapterkit = Adapterkit()
adapter_class = adapterkit.get_adapter_class_by_adapter_id(adapter_id)

# Create a temporary instance just to get configured URLs
# This will trigger URL validation in __init__ but not full connection test
return adapter_class(adapter_metadata)

except Exception as e:
logger.error(f"URL validation failed for adapter {adapter_id}: {str(e)}")
raise

@staticmethod
def __fetch_adapters_by_key_value(key: str, value: Any) -> Adapter:
"""Fetches a list of adapters that have an attribute matching key and
Expand Down
93 changes: 92 additions & 1 deletion backend/adapter_processor_v2/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import json
import logging
import uuid
from typing import Any

from cryptography.fernet import Fernet
from django.conf import settings
from django.db import IntegrityError
from django.db.models import ProtectedError, QuerySet
from django.http import HttpRequest
Expand All @@ -14,12 +17,15 @@
)
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.versioning import URLPathVersioning
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from tenant_account_v2.organization_member_service import OrganizationMemberService
from tenant_account_v2.organization_member_service import (
OrganizationMemberService,
)
from utils.filtering import FilterHelper

from adapter_processor_v2.adapter_processor import AdapterProcessor
Expand Down Expand Up @@ -177,6 +183,57 @@ def create(self, request: Any) -> Response:
use_platform_unstract_key = True

serializer.is_valid(raise_exception=True)

# Validate URLs for security without full adapter testing
adapter_id = serializer.validated_data.get(AdapterKeys.ADAPTER_ID)
adapter_metadata_b = serializer.validated_data.get(AdapterKeys.ADAPTER_METADATA_B)

if not adapter_metadata_b:
raise ValidationError("Missing adapter metadata for validation.")

# Decrypt metadata to get configuration
try:
# Validate URLs for security without full adapter testing
adapter_id = serializer.validated_data.get(AdapterKeys.ADAPTER_ID)
adapter_metadata_b = serializer.validated_data.get(AdapterKeys.ADAPTER_METADATA_B)

from rest_framework.exceptions import ValidationError
if not adapter_metadata_b:
raise ValidationError("Missing adapter metadata for validation.")

# Decrypt metadata to get configuration
try:
fernet = Fernet(settings.ENCRYPTION_KEY.encode("utf-8"))
decrypted_json = fernet.decrypt(adapter_metadata_b)
decrypted_metadata = json.loads(decrypted_json.decode("utf-8"))
# Ensure object shape
from rest_framework.exceptions import ValidationError
if not isinstance(decrypted_metadata, dict):
raise ValidationError("Invalid adapter metadata format: expected JSON object.")
except Exception as e: # InvalidToken/JSONDecodeError/TypeError/etc.
raise ValidationError("Invalid adapter metadata.") from e

# Validate URLs for this adapter configuration
try:
_ = AdapterProcessor.validate_adapter_urls(adapter_id, decrypted_metadata)
except Exception as e:
# Format error message similar to test adapter API
adapter_name = (
decrypted_metadata.get(AdapterKeys.ADAPTER_NAME, "adapter")
if isinstance(decrypted_metadata, dict)
else "adapter"
)
error_detail = f"Error testing '{adapter_name}'. {e!s}"
raise ValidationError(error_detail) from e

# Validate URLs for this adapter configuration
try:
AdapterProcessor.validate_adapter_urls(adapter_id, decrypted_metadata)
except Exception as e:
# Format error message similar to test adapter API
adapter_name = decrypted_metadata.get(AdapterKeys.ADAPTER_NAME, "adapter")
error_detail = f"Error testing '{adapter_name}'. {e!s}"
raise ValidationError(error_detail) from e
try:
adapter_type = serializer.validated_data.get(AdapterKeys.ADAPTER_TYPE)

Expand Down Expand Up @@ -359,6 +416,40 @@ def update(
# Get the adapter instance for update
adapter = self.get_object()

# Get serializer and validate data first
serializer = self.get_serializer(adapter, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)

# Validate URLs for security if metadata is being updated
if AdapterKeys.ADAPTER_METADATA_B in serializer.validated_data:
adapter_id = (
serializer.validated_data.get(AdapterKeys.ADAPTER_ID)
or adapter.adapter_id
)
adapter_metadata_b = serializer.validated_data.get(
AdapterKeys.ADAPTER_METADATA_B
)

if not adapter_id or adapter_metadata_b:
raise ValidationError("Missing adapter metadata for validation.")

# Decrypt metadata to get configuration
try:
fernet = Fernet(settings.ENCRYPTION_KEY.encode("utf-8"))
decrypted_json = fernet.decrypt(adapter_metadata_b)
decrypted_metadata = json.loads(decrypted_json.decode("utf-8"))
except Exception as e: # InvalidToken/JSONDecodeError/TypeError/etc.
raise ValidationError("Invalid adapter metadata.") from e

# Validate URLs for this adapter configuration
try:
_ = AdapterProcessor.validate_adapter_urls(adapter_id, decrypted_metadata)
except Exception as e:
# Format error message similar to test adapter API
adapter_name = decrypted_metadata.get(AdapterKeys.ADAPTER_NAME, "adapter")
error_detail = f"Error testing '{adapter_name}'. {e!s}"
raise ValidationError(error_detail) from e

if use_platform_unstract_key:
logger.error("Processing adapter with platform key")
serializer = self.get_serializer(adapter, data=request.data, partial=True)
Expand Down
5 changes: 5 additions & 0 deletions backend/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,8 @@ RUNNER_POLLING_INTERVAL_SECONDS=2
# Default: 1800 seconds (30 minutes)
# Examples: 900 (15 min), 1800 (30 min), 3600 (60 min)
MIN_SCHEDULE_INTERVAL_SECONDS=1800

# WHitelisted adapter URLs to allow user to connect to locally hosted adapters.
# Whitelisting 10.68.0.10 to allow frictionless adapter connection to
# managed Postgres for VectorDB
ALLOWED_ADAPTER_PRIVATE_ENDPOINTS="127.0.0.1, 10.68.0.10"