Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions backend/account_v2/serializer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re

from rest_framework import serializers
from utils.input_sanitizer import validate_name_field

from account_v2.models import Organization, User

Expand All @@ -10,6 +11,12 @@ class OrganizationSignupSerializer(serializers.Serializer):
display_name = serializers.CharField(required=True, max_length=150)
organization_id = serializers.CharField(required=True, max_length=30)

def validate_name(self, value: str) -> str:
return validate_name_field(value, field_name="Organization name")

def validate_display_name(self, value: str) -> str:
return validate_name_field(value, field_name="Display name")

def validate_organization_id(self, value): # type: ignore
if not re.match(r"^[a-z0-9_-]+$", value):
raise serializers.ValidationError(
Expand Down
15 changes: 15 additions & 0 deletions backend/adapter_processor_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.conf import settings
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
from utils.input_sanitizer import validate_name_field, validate_no_html_tags

from adapter_processor_v2.adapter_processor import AdapterProcessor
from adapter_processor_v2.constants import AdapterKeys
Expand All @@ -28,6 +29,20 @@ class Meta:
model = AdapterInstance
fields = "__all__"

def validate(self, data):
data = super().validate(data)
adapter_name = data.get("adapter_name")
if adapter_name is not None:
data["adapter_name"] = validate_name_field(
adapter_name, field_name="Adapter name"
)
description = data.get("description")
if description is not None:
data["description"] = validate_no_html_tags(
description, field_name="Description"
)
return data


class DefaultAdapterSerializer(serializers.Serializer):
llm_default = serializers.CharField(max_length=FLC.UUID_LENGTH, required=False)
Expand Down
9 changes: 9 additions & 0 deletions backend/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ValidationError,
)
from tags.serializers import TagParamsSerializer
from utils.input_sanitizer import validate_name_field, validate_no_html_tags
from utils.serializer.integrity_error_mixin import IntegrityErrorMixin
from workflow_manager.endpoint_v2.models import WorkflowEndpoint
from workflow_manager.workflow_v2.exceptions import ExecutionDoesNotExistError
Expand Down Expand Up @@ -62,6 +63,14 @@ def validate_api_name(self, value: str) -> str:
api_name_validator(value)
return value

def validate_display_name(self, value: str) -> str:
return validate_name_field(value, field_name="Display name")

def validate_description(self, value: str) -> str:
if value is None:
return value
return validate_no_html_tags(value, field_name="Description")

def validate_workflow(self, workflow):
"""Validate that the workflow has properly configured source and destination endpoints."""
# Get all endpoints for this workflow with related data
Expand Down
1 change: 1 addition & 0 deletions backend/backend/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ def filter(self, record):
"social_django.middleware.SocialAuthExceptionMiddleware",
"middleware.remove_allow_header.RemoveAllowHeaderMiddleware",
"middleware.cache_control.CacheControlMiddleware",
"middleware.content_security_policy.ContentSecurityPolicyMiddleware",
]

TENANT_SUBFOLDER_PREFIX = f"{PATH_PREFIX}/unstract"
Expand Down
4 changes: 4 additions & 0 deletions backend/connector_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from connector_processor.exceptions import OAuthTimeOut
from rest_framework.serializers import CharField, SerializerMethodField
from utils.fields import EncryptedBinaryFieldSerializer
from utils.input_sanitizer import validate_name_field

from backend.serializers import AuditSerializer
from connector_v2.constants import ConnectorInstanceKey as CIKey
Expand All @@ -28,6 +29,9 @@ class Meta:
model = ConnectorInstance
fields = "__all__"

def validate_connector_name(self, value: str) -> str:
return validate_name_field(value, field_name="Connector name")

def save(self, **kwargs): # type: ignore
user = self.context.get("request").user or None
connector_id: str = kwargs[CIKey.CONNECTOR_ID]
Expand Down
31 changes: 31 additions & 0 deletions backend/middleware/content_security_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.http import HttpRequest, HttpResponse
from django.utils.deprecation import MiddlewareMixin


class ContentSecurityPolicyMiddleware(MiddlewareMixin):
"""Middleware to add Content-Security-Policy header to all responses.

Since this is a JSON API backend, the policy is restrictive by default:
only 'self' is allowed for all directives, and no inline scripts or styles
are permitted. This prevents any injected content from being executed if a
response is ever rendered in a browser context.
"""

def process_response(
self, request: HttpRequest, response: HttpResponse
) -> HttpResponse:
response.setdefault(
"Content-Security-Policy",
(
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self'; "
Comment on lines +6 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CSP blocks inline scripts required by the login page.

The docstring states this is a "JSON API backend" with no inline scripts permitted, but the backend serves HTML pages with inline JavaScript. Context snippet 3 shows backend/account_v2/templates/login.html contains an inline <script> block for the form submit spinner, and context snippet 4 confirms authentication_service.py renders these templates via Django's render().

The current script-src 'self' (line 21) without 'unsafe-inline' will block that inline script, breaking the login page functionality.

🐛 Proposed fix: Add 'unsafe-inline' for script-src and style-src
     def process_response(
         self, request: HttpRequest, response: HttpResponse
     ) -> HttpResponse:
         response.setdefault(
             "Content-Security-Policy",
             (
                 "default-src 'self'; "
-                "script-src 'self'; "
-                "style-src 'self'; "
+                "script-src 'self' 'unsafe-inline'; "
+                "style-src 'self' 'unsafe-inline'; "
                 "img-src 'self'; "
                 "font-src 'self'; "
                 "connect-src 'self'; "
                 "frame-ancestors 'none'; "
                 "base-uri 'self'; "
                 "form-action 'self'"
             ),
         )
         return response

Alternatively, refactor the login template to use an external JS file or add a nonce-based CSP for stronger security. Also update the docstring to remove the misleading "JSON API backend" claim.

The AI summary states script-src 'self' 'unsafe-inline' but the actual code at line 21 shows script-src 'self' without 'unsafe-inline'.

🧰 Tools
🪛 Ruff (0.15.4)

[warning] 15-15: Unused method argument: request

(ARG002)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/middleware/content_security_policy.py` around lines 6 - 22, The CSP
middleware's process_response currently sets "script-src 'self'" and "style-src
'self'", which blocks the inline <script> used by the login page; update the
header value set in process_response to permit inline scripts/styles (e.g., add
'unsafe-inline' to both script-src and style-src) or implement a nonce-based CSP
and inject matching nonces into the login template rendered by
authentication_service.render; also update the middleware docstring to remove
the incorrect "JSON API backend" claim. Use the process_response function and
the "Content-Security-Policy" header string as the change points, or
alternatively refactor backend/account_v2/templates/login.html to use external
JS and then keep the stricter CSP.

"img-src 'self'; "
"font-src 'self'; "
"connect-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self'"
),
Comment on lines +19 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 object-src not explicitly restricted in backend CSP

object-src controls whether <object>, <embed>, and <applet> elements (Flash/Java plugins) can load content. It falls back to default-src 'self' here rather than being explicitly denied. The frontend nginx.conf correctly sets object-src 'none', but the backend middleware omits it.

Best practice (and the OWASP CSP cheat sheet recommendation) is to explicitly set object-src 'none' on all policies since plugin-based content is almost never intentional in a JSON API context:

Suggested change
(
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self'; "
"img-src 'self'; "
"font-src 'self'; "
"connect-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self'"
),
(
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self'; "
"img-src 'self'; "
"font-src 'self'; "
"connect-src 'self'; "
"object-src 'none'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self'"
),
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/middleware/content_security_policy.py
Line: 19-29

Comment:
**`object-src` not explicitly restricted in backend CSP**

`object-src` controls whether `<object>`, `<embed>`, and `<applet>` elements (Flash/Java plugins) can load content. It falls back to `default-src 'self'` here rather than being explicitly denied. The frontend nginx.conf correctly sets `object-src 'none'`, but the backend middleware omits it.

Best practice (and the OWASP CSP cheat sheet recommendation) is to explicitly set `object-src 'none'` on all policies since plugin-based content is almost never intentional in a JSON API context:

```suggestion
            (
                "default-src 'self'; "
                "script-src 'self'; "
                "style-src 'self'; "
                "img-src 'self'; "
                "font-src 'self'; "
                "connect-src 'self'; "
                "object-src 'none'; "
                "frame-ancestors 'none'; "
                "base-uri 'self'; "
                "form-action 'self'"
            ),
```

How can I resolve this? If you propose a fix, please make it concise.

)
return response
3 changes: 3 additions & 0 deletions backend/notification_v2/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from rest_framework import serializers
from utils.input_sanitizer import validate_name_field

from .enums import AuthorizationType, NotificationType, PlatformType
from .models import Notification
Expand Down Expand Up @@ -109,6 +110,8 @@ def validate_name(self, value):
"""Check uniqueness of the name with respect to either 'api' or
'pipeline'.
"""
value = validate_name_field(value, field_name="Notification name")

api = self.initial_data.get("api", getattr(self.instance, "api", None))
pipeline = self.initial_data.get(
"pipeline", getattr(self.instance, "pipeline", None)
Expand Down
7 changes: 7 additions & 0 deletions backend/prompt_studio/prompt_studio_core_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from utils.FileValidator import FileValidator
from utils.input_sanitizer import validate_name_field, validate_no_html_tags
from utils.serializer.integrity_error_mixin import IntegrityErrorMixin

from backend.serializers import AuditSerializer
Expand Down Expand Up @@ -51,6 +52,12 @@ class Meta:
}
}

def validate_tool_name(self, value: str) -> str:
return validate_name_field(value, field_name="Tool name")

def validate_description(self, value: str) -> str:
return validate_no_html_tags(value, field_name="Description")
Comment on lines +58 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing None guard — potential TypeError on nullable description

Same issue as workflow_v2/serializers.py:53validate_description does not guard against None before calling validate_no_html_tags. If the description field accepts null values, DRF will invoke this method with None, and HTML_TAG_PATTERN.search(None) will raise a TypeError.

Suggested change
def validate_description(self, value: str) -> str:
return validate_no_html_tags(value, field_name="Description")
def validate_description(self, value: str) -> str:
if value is None:
return value
return validate_no_html_tags(value, field_name="Description")
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/prompt_studio/prompt_studio_core_v2/serializers.py
Line: 58-59

Comment:
**Missing `None` guard — potential `TypeError` on nullable `description`**

Same issue as `workflow_v2/serializers.py:53``validate_description` does not guard against `None` before calling `validate_no_html_tags`. If the `description` field accepts `null` values, DRF will invoke this method with `None`, and `HTML_TAG_PATTERN.search(None)` will raise a `TypeError`.

```suggestion
    def validate_description(self, value: str) -> str:
        if value is None:
            return value
        return validate_no_html_tags(value, field_name="Description")
```

How can I resolve this? If you propose a fix, please make it concise.


def validate_summarize_llm_adapter(self, value):
"""Validate that the adapter type is LLM and is accessible to the user."""
if value is None:
Expand Down
29 changes: 29 additions & 0 deletions backend/utils/input_sanitizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import re

from rest_framework.serializers import ValidationError

# Pattern to detect HTML/script tags
HTML_TAG_PATTERN = re.compile(r"<[^>]*>")
# Pattern to detect javascript: protocol
JS_PROTOCOL_PATTERN = re.compile(r"javascript\s*:", re.IGNORECASE)
# Pattern to detect event handlers (onclick, onerror, etc.)
EVENT_HANDLER_PATTERN = re.compile(r"(?:^|\s)on\w+\s*=", re.IGNORECASE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 EVENT_HANDLER_PATTERN misses event handlers preceded by non-whitespace delimiters

The pattern (?:^|\s)on\w+\s*= only matches on<handler>= when it appears at the start of the string or is preceded by whitespace (\s). Non-whitespace delimiters like ;, &, or " won't match. For example:

"foo;onclick=bar"      # NOT caught
"foo&onerror=alert(1)" # NOT caught

In practice, event handlers need to be inside an HTML tag to be executable (e.g., <img onerror=...>), and HTML tags are already blocked by HTML_TAG_PATTERN. However, since this pattern is also tested in isolation as a defense-in-depth check, the gap could create a false sense of complete coverage.

Consider anchoring the on prefix more broadly:

EVENT_HANDLER_PATTERN = re.compile(r"\bon\w+\s*=", re.IGNORECASE)

The word boundary \b would still require the on to start at a word boundary (after non-word characters like ;, &, space, or start of string), but would not require whitespace specifically.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/utils/input_sanitizer.py
Line: 10

Comment:
**`EVENT_HANDLER_PATTERN` misses event handlers preceded by non-whitespace delimiters**

The pattern `(?:^|\s)on\w+\s*=` only matches `on<handler>=` when it appears at the start of the string or is preceded by whitespace (`\s`). Non-whitespace delimiters like `;`, `&`, or `"` won't match. For example:

```
"foo;onclick=bar"      # NOT caught
"foo&onerror=alert(1)" # NOT caught
```

In practice, event handlers need to be inside an HTML tag to be executable (e.g., `<img onerror=...>`), and HTML tags are already blocked by `HTML_TAG_PATTERN`. However, since this pattern is also tested in isolation as a defense-in-depth check, the gap could create a false sense of complete coverage.

Consider anchoring the `on` prefix more broadly:
```python
EVENT_HANDLER_PATTERN = re.compile(r"\bon\w+\s*=", re.IGNORECASE)
```
The word boundary `\b` would still require the `on` to start at a word boundary (after non-word characters like `;`, `&`, space, or start of string), but would not require whitespace specifically.

How can I resolve this? If you propose a fix, please make it concise.



def validate_no_html_tags(value: str, field_name: str = "This field") -> str:
"""Reject values containing HTML/script tags."""
if HTML_TAG_PATTERN.search(value):
raise ValidationError(f"{field_name} must not contain HTML or script tags.")
if JS_PROTOCOL_PATTERN.search(value):
raise ValidationError(f"{field_name} must not contain JavaScript protocols.")
if EVENT_HANDLER_PATTERN.search(value):
raise ValidationError(f"{field_name} must not contain event handler attributes.")
return value


def validate_name_field(value: str, field_name: str = "This field") -> str:
"""Validate name/identifier fields - no HTML tags, strip whitespace."""
value = value.strip()
if not value:
raise ValidationError(f"{field_name} must not be empty.")
return validate_no_html_tags(value, field_name)
Empty file added backend/utils/tests/__init__.py
Empty file.
97 changes: 97 additions & 0 deletions backend/utils/tests/test_input_sanitizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import pytest
from rest_framework.serializers import ValidationError

from utils.input_sanitizer import validate_name_field, validate_no_html_tags


class TestValidateNoHtmlTags:
def test_clean_input_passes(self):
assert validate_no_html_tags("Hello World") == "Hello World"

def test_allows_normal_special_chars(self):
assert (
validate_no_html_tags("My workflow (v2), test - final")
== "My workflow (v2), test - final"
)

def test_allows_numbers_and_punctuation(self):
assert validate_no_html_tags("Test 123 & more!") == "Test 123 & more!"

def test_rejects_script_tag(self):
with pytest.raises(ValidationError, match="must not contain HTML or script tags"):
validate_no_html_tags("<script>alert(1)</script>")

def test_rejects_img_tag(self):
with pytest.raises(ValidationError, match="must not contain HTML or script tags"):
validate_no_html_tags('<img src=x onerror=alert(1)>')

def test_rejects_div_tag(self):
with pytest.raises(ValidationError, match="must not contain HTML or script tags"):
validate_no_html_tags("<div>content</div>")

def test_rejects_self_closing_tag(self):
with pytest.raises(ValidationError, match="must not contain HTML or script tags"):
validate_no_html_tags("<br/>")

def test_rejects_javascript_protocol(self):
with pytest.raises(ValidationError, match="must not contain JavaScript protocols"):
validate_no_html_tags("javascript:alert(1)")

def test_rejects_javascript_protocol_with_spaces(self):
with pytest.raises(ValidationError, match="must not contain JavaScript protocols"):
validate_no_html_tags("javascript :alert(1)")

def test_rejects_javascript_protocol_case_insensitive(self):
with pytest.raises(ValidationError, match="must not contain JavaScript protocols"):
validate_no_html_tags("JAVASCRIPT:alert(1)")

def test_rejects_event_handler(self):
with pytest.raises(
ValidationError, match="must not contain event handler attributes"
):
validate_no_html_tags("onclick=alert(1)")

def test_rejects_event_handler_with_spaces(self):
with pytest.raises(
ValidationError, match="must not contain event handler attributes"
):
validate_no_html_tags("onerror =alert(1)")

def test_rejects_event_handler_case_insensitive(self):
with pytest.raises(
ValidationError, match="must not contain event handler attributes"
):
validate_no_html_tags("ONLOAD=alert(1)")

def test_custom_field_name_in_error(self):
with pytest.raises(ValidationError, match="Workflow name"):
validate_no_html_tags("<script>", field_name="Workflow name")


class TestValidateNameField:
def test_clean_name_passes(self):
assert validate_name_field("My Workflow") == "My Workflow"

def test_strips_whitespace(self):
assert validate_name_field(" hello ") == "hello"

def test_rejects_empty_after_strip(self):
with pytest.raises(ValidationError, match="must not be empty"):
validate_name_field(" ")

def test_rejects_html_tags(self):
with pytest.raises(ValidationError, match="must not contain HTML or script tags"):
validate_name_field("<script>alert(1)</script>")

def test_allows_hyphens_and_underscores(self):
assert validate_name_field("my-workflow_v2") == "my-workflow_v2"

def test_allows_periods(self):
assert validate_name_field("config.v2") == "config.v2"

def test_allows_parentheses_and_commas(self):
assert validate_name_field("Test (v2), final") == "Test (v2), final"

def test_custom_field_name_in_error(self):
with pytest.raises(ValidationError, match="Tool name"):
validate_name_field(" ", field_name="Tool name")
7 changes: 7 additions & 0 deletions backend/workflow_manager/workflow_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from tool_instance_v2.serializers import ToolInstanceSerializer
from tool_instance_v2.tool_instance_helper import ToolInstanceHelper
from utils.input_sanitizer import validate_name_field, validate_no_html_tags
from utils.serializer.integrity_error_mixin import IntegrityErrorMixin

from backend.constants import RequestKey
Expand Down Expand Up @@ -46,6 +47,12 @@ class Meta:
}
}

def validate_workflow_name(self, value: str) -> str:
return validate_name_field(value, field_name="Workflow name")

def validate_description(self, value: str) -> str:
return validate_no_html_tags(value, field_name="Description")
Comment on lines +53 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing None guard — potential TypeError on nullable description

In DRF, when a field has allow_null=True, validate_<field> is still called with None after the field returns its value. Calling validate_no_html_tags(None, ...) will propagate None into HTML_TAG_PATTERN.search(None), which raises TypeError: expected string or bytes-like object.

The exact same description field is handled safely in api_v2/serializers.py:

def validate_description(self, value: str) -> str:
    if value is None:
        return value
    return validate_no_html_tags(value, field_name="Description")

This protection is missing in both WorkflowSerializer.validate_description (here) and CustomToolSerializer.validate_description in prompt_studio/prompt_studio_core_v2/serializers.py:58.

Suggested change
def validate_description(self, value: str) -> str:
return validate_no_html_tags(value, field_name="Description")
def validate_description(self, value: str) -> str:
if value is None:
return value
return validate_no_html_tags(value, field_name="Description")
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/workflow_manager/workflow_v2/serializers.py
Line: 53-54

Comment:
**Missing `None` guard — potential `TypeError` on nullable `description`**

In DRF, when a field has `allow_null=True`, `validate_<field>` is still called with `None` after the field returns its value. Calling `validate_no_html_tags(None, ...)` will propagate `None` into `HTML_TAG_PATTERN.search(None)`, which raises `TypeError: expected string or bytes-like object`.

The exact same `description` field is handled safely in `api_v2/serializers.py`:
```python
def validate_description(self, value: str) -> str:
    if value is None:
        return value
    return validate_no_html_tags(value, field_name="Description")
```

This protection is missing in both `WorkflowSerializer.validate_description` (here) and `CustomToolSerializer.validate_description` in `prompt_studio/prompt_studio_core_v2/serializers.py:58`.

```suggestion
    def validate_description(self, value: str) -> str:
        if value is None:
            return value
        return validate_no_html_tags(value, field_name="Description")
```

How can I resolve this? If you propose a fix, please make it concise.


def to_representation(self, instance: Workflow) -> dict[str, str]:
representation: dict[str, str] = super().to_representation(instance)
representation[WorkflowKey.WF_NAME] = instance.workflow_name
Expand Down
21 changes: 21 additions & 0 deletions frontend/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,27 @@ http {
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Content Security Policy
# - 'unsafe-inline' in script-src: required for runtime-config.js injected at container start
# - 'unsafe-inline' in style-src: required by Ant Design CSS-in-JS
# - cdn.jsdelivr.net: Monaco Editor loads from this CDN
# - unpkg.com: PDF.js worker
# - PostHog, GTM, reCAPTCHA, Stripe, Product Fruits: third-party services
add_header Content-Security-Policy
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com https://eu.i.posthog.com https://eu-assets.i.posthog.com https://www.googletagmanager.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://js.stripe.com https://app.productfruits.com; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: blob: https:; "
"font-src 'self' data:; "
"connect-src 'self' ws: wss: https://eu.i.posthog.com https://eu-assets.i.posthog.com https://www.google-analytics.com https://api.stripe.com https://app.productfruits.com; "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ws: in connect-src allows unencrypted WebSocket to any origin

ws: is a scheme-only wildcard that permits WebSocket connections to any host over plaintext. In production, you almost certainly only need secure WebSockets (wss:). Leaving ws: here means a script running in the browser could open ws://any-attacker-host/exfil and it would not be blocked by CSP.

Consider tightening to just wss: (or even scoping it to specific origins like wss://your-api-host) to remove the plaintext connection escape hatch:

Suggested change
"connect-src 'self' ws: wss: https://eu.i.posthog.com https://eu-assets.i.posthog.com https://www.google-analytics.com https://api.stripe.com https://app.productfruits.com; "
"connect-src 'self' wss: https://eu.i.posthog.com https://eu-assets.i.posthog.com https://www.google-analytics.com https://api.stripe.com https://app.productfruits.com; "
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/nginx.conf
Line: 62

Comment:
**`ws:` in `connect-src` allows unencrypted WebSocket to any origin**

`ws:` is a scheme-only wildcard that permits WebSocket connections to **any host** over plaintext. In production, you almost certainly only need secure WebSockets (`wss:`). Leaving `ws:` here means a script running in the browser could open `ws://any-attacker-host/exfil` and it would not be blocked by CSP.

Consider tightening to just `wss:` (or even scoping it to specific origins like `wss://your-api-host`) to remove the plaintext connection escape hatch:

```suggestion
            "connect-src 'self' wss: https://eu.i.posthog.com https://eu-assets.i.posthog.com https://www.google-analytics.com https://api.stripe.com https://app.productfruits.com; "
```

How can I resolve this? If you propose a fix, please make it concise.

"frame-src 'self' https://www.google.com/recaptcha/ https://recaptcha.google.com https://js.stripe.com https://hooks.stripe.com; "
"worker-src 'self' blob: https://unpkg.com https://cdn.jsdelivr.net; "
"object-src 'none'; "
"base-uri 'self'; "
"form-action 'self' https://checkout.stripe.com; "
"frame-ancestors 'self'"
always;
Comment on lines +56 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hari-kuriakose I hope this covers everything we use. @vishnuszipstack please check. I believe you had some report last time


# Disable TRACE and TRACK methods
if ($request_method ~ ^(TRACE|TRACK)$) {
return 405;
Expand Down