Skip to content

[WEB-7887] fix(security): prevent stored XSS via SVG attachment served inline (GHSA-ch8j-vr4r-qf6h)#9312

Open
mguptahub wants to merge 2 commits into
previewfrom
web-7887/svg-attachment-xss
Open

[WEB-7887] fix(security): prevent stored XSS via SVG attachment served inline (GHSA-ch8j-vr4r-qf6h)#9312
mguptahub wants to merge 2 commits into
previewfrom
web-7887/svg-attachment-xss

Conversation

@mguptahub

@mguptahub mguptahub commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds SCRIPT_CAPABLE_MIME_TYPES frozenset to common.py listing MIME types browsers can execute as scripts when served inline (image/svg+xml, text/javascript, application/javascript, text/html, application/xhtml+xml, text/xml, application/xml)
  • Three download endpoints that previously defaulted to Content-Disposition: inline now check the stored asset MIME type and force attachment for any script-capable type:
    • GenericAssetEndpoint.getapps/api/plane/api/views/asset.py
    • StaticFileAssetEndpoint.getapps/api/plane/app/views/asset/v2.py
    • EntityAssetEndpoint.getapps/api/plane/space/views/asset.py
  • ATTACHMENT_MIME_TYPES is unchanged — users can still upload SVG, JS, and XML files; the fix only affects how they are served on download

Security context

Advisory: GHSA-ch8j-vr4r-qf6h | Severity: High | CWE-79 (Stored XSS)

A Guest-level workspace member could upload a malicious image/svg+xml attachment. In the default self-hosted MinIO deployment the presigned download URL is on the same origin as the Plane app. With Content-Disposition: inline, the browser renders the SVG and executes any embedded onload / <script> JavaScript in the application's security context → session theft and account takeover of any user who opened the link.

Forcing Content-Disposition: attachment for script-capable types causes the browser to download the file instead of rendering it, removing the execution context entirely.

Test plan

  • Upload an SVG file as an issue attachment — upload should succeed
  • Fetch the download URL for the SVG — verify Content-Disposition: attachment in the presigned URL response headers
  • Upload a normal image (JPEG/PNG) — verify it still serves with Content-Disposition: inline
  • Upload a JS file as a generic asset — verify download URL returns Content-Disposition: attachment
  • Verify existing non-script attachments (PDF, DOCX, MP4) are unaffected

Summary by CodeRabbit

  • Bug Fixes
    • Asset download and preview presigned links now choose Content-Disposition based on the asset’s MIME type, forcing script-capable types to download instead of opening inline.
    • Normal (non-script) file types continue to render inline for expected previews, preserving the browsing experience while improving safety.

…d inline (GHSA-ch8j-vr4r-qf6h)

Add SCRIPT_CAPABLE_MIME_TYPES frozenset (image/svg+xml, text/javascript,
application/javascript, text/html, application/xhtml+xml, text/xml,
application/xml) and enforce Content-Disposition: attachment on three
download endpoints that previously defaulted to inline serving:

- GenericAssetEndpoint.get (api/views/asset.py)
- StaticFileAssetEndpoint.get (app/views/asset/v2.py)
- EntityAssetEndpoint.get (space/views/asset.py)

ATTACHMENT_MIME_TYPES is unchanged — users can still upload SVG, JS, and
XML files. The fix closes the XSS vector by ensuring script-capable assets
are always downloaded rather than rendered in the application's origin.

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI review requested due to automatic review settings June 25, 2026 06:14

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@makeplane

makeplane Bot commented Jun 25, 2026

Copy link
Copy Markdown

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai

coderabbitai Bot commented Jun 25, 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

Run ID: 97499384-15de-4a3b-a34c-4d72958422fd

📥 Commits

Reviewing files that changed from the base of the PR and between 5a4f3bb and a836ec4.

📒 Files selected for processing (3)
  • apps/api/plane/api/views/asset.py
  • apps/api/plane/app/views/asset/v2.py
  • apps/api/plane/space/views/asset.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/api/plane/api/views/asset.py
  • apps/api/plane/app/views/asset/v2.py
  • apps/api/plane/space/views/asset.py

📝 Walkthrough

Walkthrough

Asset download endpoints now derive Content-Disposition from each asset’s MIME type and pass it into presigned URL generation. A new settings constant lists script-capable MIME types that must be served as attachments.

Changes

Asset download disposition update

Layer / File(s) Summary
Script-capable MIME types
apps/api/plane/settings/common.py
Adds SCRIPT_CAPABLE_MIME_TYPES as a frozen set of MIME types that require attachment handling.
Asset download disposition
apps/api/plane/api/views/asset.py, apps/api/plane/app/views/asset/v2.py, apps/api/plane/space/views/asset.py
The asset download endpoints read asset.attributes["type"], choose attachment for script-capable MIME types and inline otherwise, and pass disposition to generate_presigned_url.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I hopped through links both near and far,
Inline or attachment, now clear as a star.
Scripty bunnies stay tucked in a sack,
While safe little files come streaming back.
Hop hop hooray, the URLs align!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the PR's main security fix: preventing inline rendering of stored SVG/XSS-capable downloads.
Description check ✅ Passed The description is detailed and includes summary, security context, and test plan; only some template sections are omitted.
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.

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

✨ 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 web-7887/svg-attachment-xss

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/api/plane/api/views/asset.py (1)

451-460: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the disposition logic into a shared helper.

The storage/asset_mime_type/disposition block is copy-pasted verbatim across three endpoints. A single helper (e.g. disposition_for_mime(mime_type)) keeps the security policy in one place and prevents the three copies from drifting.

🤖 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/api/plane/api/views/asset.py` around lines 451 - 460, The disposition
calculation is duplicated across multiple asset endpoints, so extract the
mime-type-to-disposition policy into a shared helper and reuse it here. Add or
reuse a helper such as disposition_for_mime and call it from the asset download
logic around S3Storage.generate_presigned_url, replacing the inline
asset_mime_type/disposition block so the security rule stays centralized.
🤖 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/api/views/asset.py`:
- Around line 452-455: Normalize the MIME type before the allowlist check in the
asset serving logic so case differences and parameters do not bypass the
SCRIPT_CAPABLE_MIME_TYPES block. Update the disposition decision in the asset
view to compare a normalized value (for example, stripped of parameters and
case-normalized) and apply the same fix in the duplicated asset handlers in
app/views/asset/v2.py and space/views/asset.py, preferably via a shared helper
so the behavior stays consistent across AssetView handling.

---

Nitpick comments:
In `@apps/api/plane/api/views/asset.py`:
- Around line 451-460: The disposition calculation is duplicated across multiple
asset endpoints, so extract the mime-type-to-disposition policy into a shared
helper and reuse it here. Add or reuse a helper such as disposition_for_mime and
call it from the asset download logic around S3Storage.generate_presigned_url,
replacing the inline asset_mime_type/disposition block so the security rule
stays centralized.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30590d93-ded0-4486-883f-5c395dffba00

📥 Commits

Reviewing files that changed from the base of the PR and between 1e8f363 and 5a4f3bb.

📒 Files selected for processing (4)
  • apps/api/plane/api/views/asset.py
  • apps/api/plane/app/views/asset/v2.py
  • apps/api/plane/settings/common.py
  • apps/api/plane/space/views/asset.py

Comment thread apps/api/plane/api/views/asset.py Outdated
…check

Strip MIME parameters and lowercase before the allowlist check so that
stored values like "image/svg+xml; charset=utf-8" or "Image/SVG+XML"
are correctly identified as script-capable and served as attachment.
Applies to all three download endpoints.

Co-authored-by: Plane AI <noreply@plane.so>
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