Skip to content

fix: security hardening — iframe sandbox, CSP, webhook auth, path traversal - #44

Open
joshuajerin wants to merge 6 commits into
10xapp:mainfrom
joshuajerin:fix/iframe-sandbox-security
Open

fix: security hardening — iframe sandbox, CSP, webhook auth, path traversal#44
joshuajerin wants to merge 6 commits into
10xapp:mainfrom
joshuajerin:fix/iframe-sandbox-security

Conversation

@joshuajerin

@joshuajerin joshuajerin commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Iframe sandbox (CRITICAL): Add sandbox attribute to AI builder preview iframe, removing unrestricted camera/mic/geo access
  • CSP enforcement (HIGH): Promote Content-Security-Policy from report-only to enforced, with correct frame-src for Expo Snack
  • Google webhook verification (HIGH): Add GOOGLE_WEBHOOK_TOKEN config; Gmail verifies ?token= param, Calendar verifies X-Goog-Channel-Token header; watch creation includes the token
  • Sandbox path traversal (HIGH): Replace weak regex sanitization with posixpath.normpath + prefix validation + shlex.quote to prevent ../ traversal in E2B sandbox file browser

Test plan

  • AI builder preview still loads Expo Snack apps correctly with sandbox attribute
  • Verify CSP doesn't block any existing features (check browser console for violations)
  • Set GOOGLE_WEBHOOK_TOKEN env var and verify Gmail/Calendar webhooks accept valid token and reject missing/invalid ones
  • Existing Calendar watches will need renewal to pick up the new token
  • Test sandbox file listing with paths containing ../ — should reject with ValueError
  • Test sandbox file listing with normal paths under /home/user — should work as before

Summary by CodeRabbit

  • Security
    • Implemented webhook token verification for Gmail and Calendar integrations to ensure authenticated requests.
    • Activated Content Security Policy (CSP) enforcement with updated browser permissions.
  • Privacy / Deployment
    • Pub/Sub setup output now requires a webhook token and redacts the token in displayed endpoints.
  • Security / Sandbox
    • Strengthened sandbox path validation to prevent unsafe file access.
  • UX
    • Improved iframe sandboxing for embedded application previews.

The iframe running Expo Snack previews had no sandbox attribute and
granted geolocation/camera/microphone permissions, allowing
AI-generated code full access to the parent origin's storage,
cookies, and navigation.

Add sandbox with minimal permissions (scripts, same-origin, forms,
popups) and remove unnecessary device access grants.
1. CSP enforcement (vercel.json): Promote Content-Security-Policy from
   report-only to enforced. Add frame-src for Expo Snack preview,
   connect-src for PostHog, and wasm-unsafe-eval for Snack SDK.

2. Google webhook verification (webhooks.py, config.py, watch_manager.py):
   Add GOOGLE_WEBHOOK_TOKEN config. Gmail endpoint verifies ?token= query
   param; Calendar endpoint verifies X-Goog-Channel-Token header. Watch
   creation now includes the token so Google sends it back on notifications.

3. Sandbox path traversal fix (sandbox_files.py): Replace weak regex
   sanitization with posixpath.normpath + prefix validation + shlex.quote.
   Paths must resolve under /home/user to prevent ../ traversal.
- Remove unused `re` import (ruff F401 lint failure)
- Return HTTP 401 instead of 200 on invalid webhook tokens
- Fail fast in watch creation when GOOGLE_WEBHOOK_TOKEN is unset
- Include token in Pub/Sub setup script push endpoint
- Add google_webhook_token to test mocks to fix 2 failing tests
… 401

The outer try/except catches all Exceptions (including HTTPException)
and returns 200 OK. Auth verification must happen before this block
so the 401 actually reaches the client.
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@joshuajerin has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 19 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 15 minutes and 19 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e6d248e-eee6-43ea-a6fe-aadeac6da9eb

📥 Commits

Reviewing files that changed from the base of the PR and between 24903a5 and 6596411.

📒 Files selected for processing (2)
  • core-api/tests/unit/test_watch_manager_permanent_failures.py
  • core-web/vercel.json
📝 Walkthrough

Walkthrough

Added Google webhook token config and verification to webhook endpoints and Pub/Sub setup, tightened sandbox path sanitization, updated Calendar watch requests to include the token, adjusted an iframe to use a sandbox attribute, and switched CSP from report-only to enforced with updated directives.

Changes

Cohort / File(s) Summary
Google Webhook Token Authentication
core-api/api/config.py, core-api/api/routers/webhooks.py, core-api/api/services/syncs/watch_manager.py, core-api/setup_pubsub_subscription.py, core-api/tests/unit/test_watch_manager_permanent_failures.py
Added google_webhook_token setting. Implemented _verify_google_webhook_token and enforced token checks for Gmail (query param) and Calendar (header) webhooks returning 401 on failure. Appended token to Calendar watch request bodies. Pub/Sub setup now requires and appends token to the push endpoint (redacted in printed output). Tests updated to include token in patched settings.
Sandbox Path Sanitization
core-api/api/services/agents/sandbox_files.py
Replaced regex-based stripping with _sanitize_sandbox_path that resolves to real path, enforces containment within sandbox root (/home/user), raises ValueError for escapes, and returns shell-quoted path. Applied to listing and reading sandbox files.
Security Headers & Iframe Permissions
core-web/vercel.json, core-web/src/components/ai-app-builder/BuilderPreview.tsx
Changed CSP header from report-only to enforced and updated directives ('wasm-unsafe-eval', PostHog connect-src, Expo/snack frame-src). Replaced iframe allow permission string with sandbox="allow-scripts allow-same-origin allow-forms allow-popups".

Sequence Diagram

sequenceDiagram
    participant Client as Webhook Sender
    participant Endpoint as Gmail/Calendar Endpoint
    participant Verify as Token Verification
    participant Settings as Settings/Config
    participant Handler as Processing Logic

    Client->>Endpoint: POST request (token in query or header)
    Endpoint->>Verify: call _verify_google_webhook_token(token)
    Verify->>Settings: read google_webhook_token
    Verify->>Verify: hmac.compare_digest(provided, stored)
    alt Token valid
        Verify-->>Endpoint: valid
        Endpoint->>Handler: proceed with parsing & enqueue
        Handler-->>Client: 200 OK
    else Token invalid or missing
        Verify-->>Endpoint: invalid
        Endpoint-->>Client: 401 Unauthorized
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I guarded tokens through the night,
I fenced each path just tight and right.
Webhooks knock — I check the door,
Sandbox seals and headers roar.
The little rabbit hops with glee — secure as can be!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately captures the main security hardening changes across multiple areas: iframe sandbox restrictions, CSP policy enforcement, webhook authentication, and path traversal prevention.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
core-web/vercel.json (1)

7-8: 'wasm-unsafe-eval' can be removed from the parent CSP.

WASM evaluation for the Snack SDK occurs only within the sandboxed iframe, not in the parent page context. The iframe has its own CSP from its origin. No direct WebAssembly usage was found in the main app source code outside the iframe context, making this directive unnecessary and a minor reduction in attack surface.

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

In `@core-web/vercel.json` around lines 7 - 8, Remove the unnecessary
'wasm-unsafe-eval' token from the Content-Security-Policy header value so the
parent page's script-src no longer includes it; update the
"Content-Security-Policy" entry in vercel.json (the object with "key":
"Content-Security-Policy") to omit 'wasm-unsafe-eval' while preserving the rest
of the policy string, since WASM runs only inside the sandboxed iframe which has
its own CSP.
core-api/tests/unit/test_watch_manager_permanent_failures.py (1)

84-87: Assert the watch body carries the token.

These tests now provide google_webhook_token, but they still only assert log level. If start_calendar_watch_service_role() stops populating body["token"], both tests stay green while webhook auth breaks.

🧪 Minimal assertion to add
         with patch("api.services.syncs.watch_manager.logger") as logger_mock:
             result = start_calendar_watch_service_role(
                 user_id="user-123",
                 calendar_service=calendar_service,
                 connection_id="conn-123",
                 service_supabase=supabase,
             )
+    watch_kwargs = calendar_service.events.return_value.watch.call_args.kwargs
+    assert watch_kwargs["body"]["token"] == "test-token"

Apply the same assertion in both Calendar-watch tests.

Also applies to: 149-152

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

In `@core-api/tests/unit/test_watch_manager_permanent_failures.py` around lines 84
- 87, The tests patch api.services.syncs.watch_manager.settings with
google_webhook_token but only assert log level; update the two Calendar-watch
tests in test_watch_manager_permanent_failures.py (the ones that call
start_calendar_watch_service_role()) to also assert that the outgoing watch
request body includes the token by checking body["token"] == "test-token" (or
equivalent access pattern used in the mocked request payload) so the tests fail
if start_calendar_watch_service_role() stops populating the token.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core-api/api/services/agents/sandbox_files.py`:
- Around line 21-35: The _sanitize_sandbox_path function currently uses
posixpath.normpath which doesn't resolve symlinks; update it to call
os.path.realpath (or posixpath.realpath if path handling is POSIX-only) on the
incoming path to canonicalize symlinks, then enforce the sandbox boundary by
checking the realpath is equal to or startswith the canonical _SANDBOX_ROOT, and
finally return the shell-quoted canonical path; update the boundary check in
_sanitize_sandbox_path to use the realpath result and add a regression test that
creates a symlink inside the sandbox pointing outside and asserts list/read
operations are rejected.

In `@core-api/api/services/syncs/watch_manager.py`:
- Around line 801-804: The renewal code builds request_body without always
adding settings.google_webhook_token which causes service-role Calendar
notifications to 401 in calendar_webhook(); update the renewal logic to mirror
start_calendar_watch() by always including request_body['token'] =
settings.google_webhook_token when settings.google_webhook_token is set (do not
conditionally omit it), ensuring the token is present on the service-role
Calendar watch creation so calendar_webhook() can validate incoming
notifications; check the code paths that construct request_body and align them
with start_calendar_watch() behavior.

In `@core-api/setup_pubsub_subscription.py`:
- Around line 42-47: The push endpoint currently concatenates the raw
webhook_token into push_endpoint and prints the full URL; instead build the URL
using urllib.parse.urlencode to safely encode the token (e.g., encode {"token":
webhook_token}) when constructing push_endpoint and avoid printing the secret:
keep push_endpoint as the fully encoded URL but replace all display/print usages
that currently print push_endpoint (references in this file) with a redacted
representation (e.g., replace token value with "<redacted>" or show only a
fingerprint) so logs never contain the plain token; update the code paths that
set webhook_token and push_endpoint (symbols: webhook_token, push_endpoint) and
the three print/log calls that output the URL to use the redacted string for
display.

---

Nitpick comments:
In `@core-api/tests/unit/test_watch_manager_permanent_failures.py`:
- Around line 84-87: The tests patch api.services.syncs.watch_manager.settings
with google_webhook_token but only assert log level; update the two
Calendar-watch tests in test_watch_manager_permanent_failures.py (the ones that
call start_calendar_watch_service_role()) to also assert that the outgoing watch
request body includes the token by checking body["token"] == "test-token" (or
equivalent access pattern used in the mocked request payload) so the tests fail
if start_calendar_watch_service_role() stops populating the token.

In `@core-web/vercel.json`:
- Around line 7-8: Remove the unnecessary 'wasm-unsafe-eval' token from the
Content-Security-Policy header value so the parent page's script-src no longer
includes it; update the "Content-Security-Policy" entry in vercel.json (the
object with "key": "Content-Security-Policy") to omit 'wasm-unsafe-eval' while
preserving the rest of the policy string, since WASM runs only inside the
sandboxed iframe which has its own CSP.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 234b449b-f8f8-4473-bdf0-2ba6fe95b86d

📥 Commits

Reviewing files that changed from the base of the PR and between 08caddc and 3aac062.

📒 Files selected for processing (8)
  • core-api/api/config.py
  • core-api/api/routers/webhooks.py
  • core-api/api/services/agents/sandbox_files.py
  • core-api/api/services/syncs/watch_manager.py
  • core-api/setup_pubsub_subscription.py
  • core-api/tests/unit/test_watch_manager_permanent_failures.py
  • core-web/src/components/ai-app-builder/BuilderPreview.tsx
  • core-web/vercel.json

Comment thread core-api/api/services/syncs/watch_manager.py Outdated
Comment thread core-api/setup_pubsub_subscription.py Outdated
- Use posixpath.realpath() instead of normpath() to prevent symlink sandbox escapes
- Require GOOGLE_WEBHOOK_TOKEN for calendar webhook watches
- URL-encode webhook token and redact from logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
core-api/api/services/syncs/watch_manager.py (1)

265-267: Normalize token once before validation/use.

Consider centralizing token retrieval with .strip() (shared helper) so whitespace-only values are rejected consistently and both call paths stay in sync.

♻️ Suggested refactor
+def _require_google_webhook_token() -> str:
+    token = settings.google_webhook_token.strip()
+    if not token:
+        raise ValueError("GOOGLE_WEBHOOK_TOKEN is required for Calendar webhook watches")
+    return token
+
 ...
-        if not settings.google_webhook_token:
-            raise ValueError("GOOGLE_WEBHOOK_TOKEN is required for Calendar webhook watches")
-        request_body['token'] = settings.google_webhook_token
+        request_body['token'] = _require_google_webhook_token()
 ...
-        if not settings.google_webhook_token:
-            raise ValueError("GOOGLE_WEBHOOK_TOKEN is required for Calendar webhook watches")
-        request_body['token'] = settings.google_webhook_token
+        request_body['token'] = _require_google_webhook_token()

Also applies to: 803-805

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

In `@core-api/api/services/syncs/watch_manager.py` around lines 265 - 267,
Normalize and validate the Google webhook token once before use: create a small
helper (e.g., get_normalized_google_webhook_token or
normalize_google_webhook_token) that reads settings.google_webhook_token, calls
.strip(), raises ValueError if the result is empty, and returns the stripped
token; then replace direct uses (the check/raise that references
settings.google_webhook_token and the assignment to request_body['token'] as
well as the other occurrences around lines 803-805) to call this helper and use
its returned token so whitespace-only values are rejected consistently and both
call paths stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core-api/api/services/agents/sandbox_files.py`:
- Around line 21-35: _sanitize_sandbox_path currently uses posixpath.realpath
which does not resolve symlinks, so update it to only perform path normalization
and boundary checks (remove the misleading "Resolve symlinks" comment) and avoid
claiming symlink resolution; then implement actual canonical/symlink validation
inside the sandbox filesystem before any file access by calling the sandbox's
resolver (e.g., use an in-sandbox command or API like sandbox.commands.run to
run a trusted canonicalization such as readlink -f or the sandbox's
resolve_canonical helper) and only accept the path if that in-sandbox canonical
path is under _SANDBOX_ROOT; keep returning a safely shell-quoted path from
_sanitize_sandbox_path but ensure the real symlink check happens in-sandbox
prior to using the quoted path in sandbox.commands.run.

In `@core-api/setup_pubsub_subscription.py`:
- Around line 92-97: The error path prints raw stderr which can leak the
--push-endpoint token; before any print/log of gcloud output sanitize the stderr
(and stdout if printed) the same way you build display_cmd: replace occurrences
of the original push endpoint value with redacted_push_endpoint (use the same
redaction logic used when building display_cmd from create_cmd) so that when you
print stderr you never output the raw token; update the code paths around the
variables create_cmd, display_cmd, redacted_push_endpoint and where stderr is
printed to perform this replacement.

---

Nitpick comments:
In `@core-api/api/services/syncs/watch_manager.py`:
- Around line 265-267: Normalize and validate the Google webhook token once
before use: create a small helper (e.g., get_normalized_google_webhook_token or
normalize_google_webhook_token) that reads settings.google_webhook_token, calls
.strip(), raises ValueError if the result is empty, and returns the stripped
token; then replace direct uses (the check/raise that references
settings.google_webhook_token and the assignment to request_body['token'] as
well as the other occurrences around lines 803-805) to call this helper and use
its returned token so whitespace-only values are rejected consistently and both
call paths stay in sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0830d0f-adb4-4019-ba76-7e723be00f9e

📥 Commits

Reviewing files that changed from the base of the PR and between 3aac062 and 24903a5.

📒 Files selected for processing (3)
  • core-api/api/services/agents/sandbox_files.py
  • core-api/api/services/syncs/watch_manager.py
  • core-api/setup_pubsub_subscription.py

Comment thread core-api/api/services/agents/sandbox_files.py
Comment thread core-api/setup_pubsub_subscription.py
@sonarqubecloud

Copy link
Copy Markdown

@joshuajerin

joshuajerin commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

@benwxng ready for review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant