Skip to content

[SECUR-245] fix(security): guard PDF image srcs and authenticate /convert-document/ (GHSA-55gq-rf47-9pqx) - #9540

Open
mguptahub wants to merge 2 commits into
previewfrom
secur-245/live-pre-auth-ssrf
Open

[SECUR-245] fix(security): guard PDF image srcs and authenticate /convert-document/ (GHSA-55gq-rf47-9pqx)#9540
mguptahub wants to merge 2 commits into
previewfrom
secur-245/live-pre-auth-ssrf

Conversation

@mguptahub

@mguptahub mguptahub commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes the two issues reported as GHSA-55gq-rf47-9pqx in apps/live, plus the one API-side change needed to keep the existing caller working.

Work item: SECUR-245

1. SSRF via PDF image rendering

The image node renderer handed node.attrs.src straight to @react-pdf/image, which fetch()es any URL with a host and fs.readFile()s a bare path. The Live container shares a Docker network with api, plane-db, plane-redis, plane-mq and plane-minio, so page content could drive requests at internal-only services.

Adds apps/live/src/lib/url-security.ts and routes both <Image> call sites through it; anything unsafe renders a placeholder.

The existing imageComponent guard was not a usable model. Its check is !src.startsWith("http") && !src.startsWith("data:") — and http://api:8000/ and http://plane-minio:9000/ both pass it. The scheme is irrelevant here; the destination is what has to be judged. That check is replaced rather than copied.

Rejected: non-http(s)/data schemes · bare and relative filesystem paths · loopback · RFC1918 · CGNAT 100.64/10 · link-local incl. 169.254.169.254 · multicast/reserved/test ranges · IPv6 ULA, link-local, site-local, multicast, NAT64, 6to4, Teredo, IPv4-mapped · obfuscated encodings (2130706433, 0x7f000001, 127.1) · single-label hosts (the shape of a Compose service name) · .local/.internal/.lan suffixes · embedded credentials · control-character scheme smuggling.

The IPv6 ranges are deliberately kept in step with the Python guard's _BLOCKED_NETWORKS in apps/api/plane/utils/ip_address.py. The first draft here was missing Teredo 2001::/32 and fec0::/10 — two implementations of one policy drifting apart is how this class of bug keeps coming back.

2. Unauthenticated /convert-document/

requireSecretKey existed but was applied to no controller, leaving an expensive HTML → Y.js conversion open to anyone who could reach the service (CWE-306). It is now applied.

Its only caller is the API's copy_s3_object duplication task, which sent headers=None. So this PR also:

  • sends the shared secret from that task, and
  • wires LIVE_SERVER_SECRET_KEY into Django settings — it was previously only in .env.example.

A missing key short-circuits with a logged misconfiguration instead of firing a request that can only 401.

Two corrections to the advisory

/pdf-export/ is not unauthenticated and is deliberately untouched. parseRequest rejects requests with no Cookie and forwards that cookie to the API to fetch the page, so the API enforces page permissions. Gating it on the shared secret would break the browser client that design implies.

The pre-auth chain does not connect. /convert-document/ performs no outbound fetch, and its output is never fed to /pdf-export/, which reads content from the API by pageId. Triggering the SSRF requires a valid session, so this is PR:L, not the reported pre-auth. Suggest reconciling severity before publishing — GitHub metadata says CVSS 8.6 while the advisory body says 7.5.

Known residual — please read before approving

DNS rebinding on the http(s) path is NOT closed here. An attacker-controlled hostname that resolves to a blocked address still gets fetched: the renderer is synchronous and the fetch() happens inside @react-pdf/image, so the resolved address cannot be pinned. This is documented in the helper's doc comment.

The proper fix is to pre-fetch raw image nodes into data: URIs exactly as imageComponent already pre-fetches assets (pdf-export.service.ts:229-230), then accept only data: at render time. Tracked as a follow-up to SECUR-245. Flagging it so this is not read as "SSRF fully fixed".

Deployment note

Gating /convert-document/ means duplication degrades if the API lacks the key: if external_data: prevents corruption, but the duplicate keeps a stale description_binary, so its images break.

Verified LIVE_SERVER_SECRET_KEY reaches api/worker/beat-worker via x-app-env in deployments/cli/community/docker-compose.yml, and that aio auto-generates it on first boot. Not verified: the Helm charts (separate repo) — worth confirming before release so self-hosted K8s users don't hit this.

Testing

  • 78 new Live tests (apps/live/tests/lib/url-security.test.ts) covering every payload named in the advisory, plus range boundaries (e.g. 100.63.x allowed / 100.64.x blocked) so the CIDR edges are pinned.
  • 6 new API tests (test_copy_s3_object_auth.py) for the header contract and the missing-key / no-live-url paths. The pre-existing duplication test mocks sync_with_external_service entirely, so this path had no coverage.
  • Full Live suite: 110 pass. tsc --noEmit: 24 errors before and after — zero introduced, all pre-existing unbuilt @plane/logger/@plane/decorators. oxlint 0 errors, oxfmt and ruff clean.

Out of scope, worth follow-ups

  • requireSecretKey compares with !== (auth-middleware.ts:38) — not constant-time. Pre-existing, but it now actually guards something, so crypto.timingSafeEqual is warranted. Its docstring also claims it accepts x-admin-secret-key "(preferred)" while the code only reads live-server-secret-key.
  • <Link src={href}> (node-renderers.tsx:65) is unguarded. Not SSRF (no fetch), but it embeds arbitrary URIs including javascript: into PDF link annotations.

Co-authored-by: Plane AI noreply@plane.so

Summary by CodeRabbit

  • Security

    • Protected document conversion requests with server authentication.
    • Blocked unsafe, internal, malformed, or non-image URLs from PDF rendering.
    • Added safer handling for unavailable images with clear placeholders.
  • Bug Fixes

    • Improved server-to-server conversion requests with authentication and bounded timeouts.
    • Added handling for missing configuration, timeouts, and unsuccessful responses.
  • Tests

    • Added comprehensive coverage for authentication, URL validation, image sources, timeouts, and error scenarios.

…vert-document/ (GHSA-55gq-rf47-9pqx)

The Live service exposed two problems reported as GHSA-55gq-rf47-9pqx.

1. SSRF via PDF image rendering. The `image` node renderer passed
   `node.attrs.src` straight to `@react-pdf/image`, which fetch()es any URL
   with a host and fs.readFile()s a bare path. Because the Live container
   shares a Docker network with api, db, redis, rabbitmq and minio, page
   content could drive requests at internal-only services.

   Adds `apps/live/src/lib/url-security.ts` and routes both `<Image>` call
   sites through it. Unsafe srcs render a placeholder instead.

   Note the existing `imageComponent` check was not a usable model: its
   `startsWith("http")` test passes `http://api:8000/` and
   `http://plane-minio:9000/` — every payload that matters. The scheme is
   irrelevant; the destination is what has to be judged. That check is
   replaced too.

   Blocked: non-http(s)/data schemes, bare and relative filesystem paths,
   loopback, RFC1918, CGNAT 100.64/10, link-local incl. 169.254.169.254,
   multicast, reserved and test ranges, IPv6 ULA/link-local/site-local/
   multicast/NAT64/6to4/Teredo/IPv4-mapped, obfuscated encodings
   (2130706433, 0x7f000001, 127.1), single-label hosts (the shape of a
   Compose service name), .local/.internal/.lan suffixes, embedded
   credentials, and control-character scheme smuggling.

   The IPv6 ranges are kept in step with the Python guard's
   _BLOCKED_NETWORKS in apps/api/plane/utils/ip_address.py; the first draft
   here was missing Teredo and fec0::/10, and two implementations of one
   policy drifting apart is how this class of bug keeps recurring.

2. Unauthenticated /convert-document/. `requireSecretKey` existed but was
   applied to no controller, leaving an expensive HTML -> Y.js conversion
   open to anyone who could reach the service (CWE-306). It is now applied.
   Its only caller — the API's copy_s3_object duplication task — sent
   `headers=None`, so it now sends the shared secret, and
   LIVE_SERVER_SECRET_KEY is wired into Django settings (it was previously
   only in .env.example). A missing key short-circuits with a logged
   misconfiguration rather than firing a request that can only 401.

Scope notes:

- /pdf-export/ is deliberately untouched. Contrary to the advisory it is
  not unauthenticated: it requires a Cookie and forwards it to the API to
  fetch the page, so the API enforces page permissions. Gating it on the
  shared secret would break the browser client that design implies.
- The advisory's pre-auth chain does not connect. /convert-document/
  performs no outbound fetch, and its output is never fed to /pdf-export/,
  which reads content from the API by pageId. The SSRF requires a valid
  session, so severity is PR:L rather than the reported pre-auth.
- Residual DNS rebinding on the http(s) path is documented in the helper
  and NOT closed here: the renderer is synchronous and the fetch happens
  inside @react-pdf/image, so the resolved address cannot be pinned.
  Closing it means pre-fetching raw image nodes into data: URIs the way
  imageComponent already pre-fetches assets. Follow-up to SECUR-245.

Tests: 78 new Live tests covering every advisory payload plus range
boundaries, and 6 API tests for the header contract and the
missing-key/no-live-url paths.

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI lite review requested due to automatic review settings August 4, 2026 11:39
@makeplane

makeplane Bot commented Aug 4, 2026

Copy link
Copy Markdown

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 7dd9883.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0242aced-f689-40d3-8062-9b5a1904a542

📥 Commits

Reviewing files that changed from the base of the PR and between e92fdf9 and 7dd9883.

📒 Files selected for processing (4)
  • apps/api/plane/bgtasks/copy_s3_object.py
  • apps/api/plane/tests/unit/bg_tasks/test_copy_s3_object_auth.py
  • apps/live/src/lib/url-security.ts
  • apps/live/tests/lib/url-security.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/live/tests/lib/url-security.test.ts
  • apps/live/src/lib/url-security.ts
  • apps/api/plane/bgtasks/copy_s3_object.py

📝 Walkthrough

Walkthrough

This PR adds LIVE_SERVER_SECRET_KEY authentication for API-to-Live conversion requests and protects the Live conversion endpoint. It also adds SSRF-blocking image URL validation and applies it to PDF image rendering.

Changes

Live service authentication and SSRF protection

Layer / File(s) Summary
API-side secret configuration and request signing
apps/api/plane/settings/common.py, apps/api/plane/bgtasks/copy_s3_object.py, apps/api/plane/tests/unit/bg_tasks/test_copy_s3_object_auth.py
Adds the Live secret setting. Conversion requests send the secret header and use bounded timeouts. Missing configuration and request failures return empty results. Tests cover authentication, timeouts, failures, and request variants.
Live controller secret-key middleware
apps/live/src/controllers/document.controller.ts
Applies requireSecretKey to the document conversion POST / route.
URL security validation utilities
apps/live/src/lib/url-security.ts, apps/live/tests/lib/url-security.test.ts
Adds validation for private, reserved, loopback, multicast, and obfuscated IP addresses, internal hostnames, unsafe schemes, credentials, malformed URLs, and non-image data URIs. Tests cover blocked and valid URL forms.
PDF renderer integration of image URL validation
apps/live/src/lib/pdf/node-renderers.tsx
Applies isSafeImageSrc to both image renderers. Unsafe sources render an [Image unavailable] placeholder.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant API as sync_with_external_service
  participant Live as document conversion endpoint
  participant Renderer as PDF image renderer

  API->>Live: POST conversion request with secret header
  Live->>Live: validate secret with requireSecretKey
  Live->>Renderer: render document images
  Renderer->>Renderer: validate image source with isSafeImageSrc
  alt unsafe image source
    Renderer-->>Live: render [Image unavailable] placeholder
  else safe image source
    Renderer-->>Live: render image
  end
  Live-->>API: return conversion response
Loading

Suggested reviewers: dheeru0198

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main security changes: PDF image URL protection and authentication for the document conversion endpoint.
Description check ✅ Passed The description explains the security issues, implementation, residual risks, deployment notes, references, and validation results in sufficient detail.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch secur-245/live-pre-auth-ssrf

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/live/src/lib/url-security.ts (1)

120-133: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Public IPv6 literal hosts are rejected by the dotless-host rule.

parsed.hostname for an IPv6 URL is bracketed, for example [2606:4700:4700::1111]. It contains no dot, so Line 128 returns false even after isBlockedHostLiteral classifies the address as allowed. The test at apps/live/tests/lib/url-security.test.ts Lines 191-195 asserts the opposite classification at the literal level, so the two layers disagree.

The direction is safe. If the rejection is intentional, state it in the comment and lock it with an isSafeImageSrc test. If it is not intentional, skip the dotless rule for bracketed literals.

♻️ Optional: apply the dotless rule only to DNS names
 const isAllowedHostname = (hostname: string): boolean => {
   const host = hostname.toLowerCase();
 
   if (!host) return false;
   if (BLOCKED_HOST_EXACT.has(host)) return false;
   if (BLOCKED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix))) return false;
   if (isBlockedHostLiteral(host)) return false;
+  // Bracketed IPv6 literals carry no dot; they were already judged above.
+  if (host.startsWith("[") && host.endsWith("]")) return true;
   // No dot => single-label => container/service name on the internal network.
   if (!host.includes(".")) return false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/live/src/lib/url-security.ts` around lines 120 - 133, Update
isAllowedHostname so bracketed IPv6 literals classified as allowed by
isBlockedHostLiteral are not rejected by the dotless-host check; apply that rule
only to DNS names. Preserve blocking for disallowed literals, single-label
hostnames, and trailing-dot hostnames, and align the isSafeImageSrc tests with
the resulting behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/plane/bgtasks/copy_s3_object.py`:
- Line 92: Update the Live request in the copy S3 object flow by passing a
configured bounded connect/read timeout tuple to requests.post. Use the existing
timeout configuration pattern if available, and update
test_copy_s3_object_auth.py to assert that requests.post receives the timeout
argument.

---

Nitpick comments:
In `@apps/live/src/lib/url-security.ts`:
- Around line 120-133: Update isAllowedHostname so bracketed IPv6 literals
classified as allowed by isBlockedHostLiteral are not rejected by the
dotless-host check; apply that rule only to DNS names. Preserve blocking for
disallowed literals, single-label hostnames, and trailing-dot hostnames, and
align the isSafeImageSrc tests with the resulting behavior.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 384e96ff-e215-482b-bc78-681791a871d9

📥 Commits

Reviewing files that changed from the base of the PR and between 1942665 and e92fdf9.

📒 Files selected for processing (7)
  • apps/api/plane/bgtasks/copy_s3_object.py
  • apps/api/plane/settings/common.py
  • apps/api/plane/tests/unit/bg_tasks/test_copy_s3_object_auth.py
  • apps/live/src/controllers/document.controller.ts
  • apps/live/src/lib/pdf/node-renderers.tsx
  • apps/live/src/lib/url-security.ts
  • apps/live/tests/lib/url-security.test.ts

Comment thread apps/api/plane/bgtasks/copy_s3_object.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses security advisory GHSA-55gq-rf47-9pqx by (1) hardening PDF export image src handling in the Live service to reduce SSRF/local-file-read risk, and (2) enforcing server-to-server authentication on Live’s /convert-document/ endpoint while updating the API background task that calls it.

Changes:

  • Introduces isSafeImageSrc / isBlockedHostLiteral SSRF guard logic and routes PDF image rendering through it with placeholders for unsafe sources.
  • Applies requireSecretKey middleware to /convert-document/ and updates the API duplication task to send live-server-secret-key, including new API setting wiring.
  • Adds extensive Live unit tests for URL filtering and API unit tests for the new header contract / misconfiguration behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/live/src/lib/url-security.ts Adds URL/IP/scheme validation helpers used to gate PDF image fetching.
apps/live/tests/lib/url-security.test.ts Adds unit tests covering advisory payloads and edge cases for the new URL guard.
apps/live/src/lib/pdf/node-renderers.tsx Uses isSafeImageSrc to block unsafe image sources and render placeholders.
apps/live/src/controllers/document.controller.ts Protects /convert-document/ with requireSecretKey middleware.
apps/api/plane/settings/common.py Adds LIVE_SERVER_SECRET_KEY to Django settings from env.
apps/api/plane/bgtasks/copy_s3_object.py Sends the shared secret header (or short-circuits loudly when missing) for /convert-document/.
apps/api/plane/tests/unit/bg_tasks/test_copy_s3_object_auth.py Adds unit tests asserting the new auth/header behavior and failure modes.

Comment thread apps/live/src/lib/url-security.ts Outdated
Comment thread apps/live/tests/lib/url-security.test.ts
Comment thread apps/live/tests/lib/url-security.test.ts
…Live request timeout

Addresses review on #9540.

Copilot: the IPv4 blocklist tested only the second octet for blocks that are
actually /24s inside public /16s, so it blackholed real public space. Reviewing
the whole class rather than the one reported case found three instances, not one:

  192.0.0.0/16   -> 192.0.0.0/24 + 192.0.2.0/24   (was blocking 192.0.3.x etc.)
  198.51.0.0/16  -> 198.51.100.0/24               (was blocking 198.51.99.x etc.)
  203.0.0.0/16   -> 203.0.113.0/24                (was blocking 203.0.112.x etc.)

Now matched on the third octet. Verified against the Python guard's is_blocked_ip
(apps/api/plane/utils/ip_address.py) for all 15 boundary cases — TS and Python
verdicts now agree exactly, which is the property the original comment claimed
but did not hold. This was the same implementation-drift failure the helper's own
comment warns about, one commit later.

Tests added in both directions so a future edit cannot silently over-block:
the four reserved /24s must be rejected, and the six adjacent public /24s must
still be allowed. The second assertion is what caught this.

CodeRabbit: requests has no default timeout, so the Live call could pin a Celery
worker indefinitely on a server that accepts the connection then stalls. Adds
LIVE_REQUEST_TIMEOUT = (5, 30) and asserts it is passed. Also asserts a ReadTimeout
degrades duplication rather than failing the task — requests.Timeout subclasses
RequestException, so the existing handler already covers it.

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI review requested due to automatic review settings August 4, 2026 11:57
@mguptahub

Copy link
Copy Markdown
Collaborator Author

Both review findings addressed in 7dd98832f5. Thanks — one of them was a genuine bug in the new guard, and it was worse than reported.

@copilot — over-blocking reserved ranges ✅ fixed, and it was 3 cases not 1

Correct, and the pattern held in two more places I'd written the same way. The blocks in question are /24s sitting inside otherwise-public /16s, but I was matching on the second octet only:

Range Intended Actually blocked Collateral
192.0.0.0/24 + 192.0.2.0/24 two /24s all of 192.0.0.0/16 192.0.1.x, 192.0.3.x
198.51.100.0/24 one /24 all of 198.51.0.0/16 198.51.99.x, 198.51.101.x
203.0.113.0/24 one /24 all of 203.0.0.0/16 203.0.112.x, 203.0.114.x

Now matched on the third octet. Failure mode was silent — legitimate images would just stop rendering with no error.

Verified against the Python guard rather than by inspection. Ran is_blocked_ip from apps/api/plane/utils/ip_address.py over all 15 boundary cases; TS and Python verdicts now agree exactly:

192.0.0.1      blocked      192.0.1.1     allowed
192.0.2.1      blocked      192.0.3.1     allowed
198.51.100.1   blocked      198.51.99.1   allowed
203.0.113.1    blocked      203.0.114.1   allowed
100.64.0.1     blocked      100.63.0.1    allowed
172.31.255.254 blocked      172.32.0.1    allowed

Worth naming: url-security.ts carries a comment warning that the TS and Python lists must not drift — and it shipped with three drifts already in it. The comment was aspirational, not true. It is now actually verified.

@copilot — missing test coverage for those ranges ✅ added

Added assertions in both directions, which is what matters here: the four reserved /24s must be rejected, and the six adjacent public /24s must still be allowed. The second group is what caught the bug — I added the tests before the fix and watched still allows the public space surrounding those /24s fail. A reject-only test would have passed against the broken code.

@coderabbitai — unbounded requests.post ✅ fixed

Correct and a real availability issue: requests has no default timeout, so a Live server that accepts the connection then stalls would pin a Celery worker indefinitely. This pre-dated the PR (the line was headers=None), but I touched it, so it's in scope.

Added LIVE_REQUEST_TIMEOUT = (5, 30) — short connect, generous read since converting a large document is legitimately slow — and asserted the argument is passed, as suggested.

Also added test_timeout_is_swallowed_not_raised: requests.Timeout subclasses RequestException, so the existing handler already returns {} and duplication degrades rather than the task failing. Now pinned by a test instead of relying on the exception hierarchy holding.

Gates

Live 112 pass (80 in url-security.test.ts) · API 8 pass · oxlint 0 errors · oxfmt and ruff clean. Re-ran everything after the pre-commit hook's auto-fixes.

The DNS-rebinding residual on the http(s) path is still open and still deliberate — see the original description. Neither review touched it, so flagging again that this PR does not close it.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@mguptahub Thanks for the detailed verification.

The timeout fix prevents stalled Live requests from indefinitely occupying Celery workers. The (5, 30) timeout and the requests.Timeout regression test cover the availability concern.

The boundary tests for both blocked and adjacent allowed ranges correctly prevent the /16 over-blocking regression.

🐇 ✅


✏️ Learnings added
Learnt from: mguptahub
URL: https://github.com/makeplane/plane/pull/9540

Timestamp: 2026-08-04T11:57:59.759Z
Learning: In `apps/live/src/lib/url-security.ts`, DNS rebinding for HTTP(S) PDF image URLs remains a deliberate, documented residual risk. Do not report it as an unaddressed regression for this PR unless the project security requirements change.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

apps/live/src/lib/url-security.ts:166

  • isSafeImageSrc trims the input before checking for whitespace/control characters, so leading/trailing whitespace (including NBSP/BOM) is silently removed and a URL like " https://images.example.com/x.png" would be accepted. This contradicts the intent of rejecting scheme-smuggling whitespace/control chars; consider failing closed when trimming changes the string and also rejecting NBSP/BOM inside the URL.
  const trimmed = src.trim();
  if (!trimmed) return false;

  // Reject control characters and whitespace, which URL parsers strip and
  // which have historically been used to smuggle a scheme past naive checks.

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.

2 participants