* [WEB-8283] fix: bind Spaces board object IDs to the anchor's project
The public Spaces board endpoints resolved the DeployBoard from the URL
anchor but trusted the caller-supplied issue_id/comment_id/intake_id
verbatim, without verifying the object belonged to that board's
project/workspace. Any authenticated user could write comments,
reactions and votes onto arbitrary issues cross-tenant, and read EXTERNAL
comments from a different project in the same workspace.
Bind every caller-supplied object id to the board's project + workspace
before writing:
- comment / issue-reaction / vote create: require the issue to exist in
the board's project via Issue.issue_objects (excludes draft/archived/
triage), else 404.
- comment-reaction create: require the comment to exist in the board's
project with access="EXTERNAL", else 404.
- intake create: require the URL intake_id to match the board's intake,
else 400.
- comment list read: scope the queryset to the board's project_id.
Also add the missing is_votes_enabled gate on vote create for parity with
comment/reaction create (pre-existing gap in the same method).
Adds contract regression tests (fail-before verified): cross-tenant
writes and the cross-project comment read now rejected, with positive
controls confirming legitimate board writes/reads still succeed.
Co-authored-by: Plane AI <noreply@plane.so>
* [WEB-8283] test: address Copilot review — cross-workspace + votes-disabled coverage
- Add cross-workspace write test (issue in a different workspace) to
exercise the workspace_id binding, matching the advisory's cross-tenant
impact (previously only same-workspace/different-project was covered).
- Add a regression test for the new is_votes_enabled gate on vote create
(votes-disabled board → 400), preventing the pre-existing gap from
reappearing.
- Clarify the section header comment: cross-tenant writes return 404 for
issue/comment binding, 400 for the intake binding mismatch.
Co-authored-by: Plane AI <noreply@plane.so>
* [WEB-8283] refactor: extract board-scope guards into shared helpers
Address CodeRabbit review: the identical "object belongs to the board's
project+workspace" existence check was duplicated across four create()
methods (in four different ViewSets). Extract two module-level helpers —
_issue_in_board_scope and _comment_in_board_scope — so the check is a
single source of truth and cannot drift between endpoints or be forgotten
on a new one (the exact class of bug this PR fixes).
Behavior-preserving; 14 contract tests still green.
Co-authored-by: Plane AI <noreply@plane.so>
* chore(security): drop advisory identifiers from code comments
Explanations kept unchanged; only the IDs are removed.
Co-authored-by: Plane AI <noreply@plane.so>
* fix(security): use the deploy board's project_id in the reaction-create activity log
CommentReactionPublicViewSet.create() logged the activity with
str(self.kwargs.get("project_id", None)) — this route's URL only ever
supplies anchor and comment_id, never project_id, so every comment
reaction created on a public board logged project_id="None", silently
corrupting the activity/audit trail. destroy() on the same viewset
already resolves the correct project_id from the deploy board; create()
now does the same.
Co-authored-by: Plane AI <noreply@plane.so>
* [WEB-8283] fix: apply board-scope guards to comment/reaction read, update and delete paths
The board/issue/external-comment scoping added by this PR's create() methods
was never applied to the list, update and delete paths built on the same
models. A caller could read reactions on an INTERNAL (non-public) comment
through the public reaction list, or reach a comment or reaction they
authored through a board it doesn't actually belong to via partial_update()
or destroy(), since those methods looked up objects by pk/actor only.
Bind IssueCommentPublicViewSet.partial_update()/destroy() to the board's
project, workspace, issue_id and EXTERNAL access; bind
IssueReactionPublicViewSet.destroy() to the board's project (previously only
workspace-scoped); and bind CommentReactionPublicViewSet.get_queryset()/
destroy() to EXTERNAL comments only. Add regression coverage for each gap,
plus positive controls confirming legitimate reads/writes on the board's own
objects still work.
Co-authored-by: Plane AI <noreply@plane.so>
---------
Co-authored-by: Plane AI <noreply@plane.so>
* fix(api): enforce FILE_SIZE_LIMIT on published Space asset upload
The public Space asset upload endpoint
(POST /api/public/assets/v2/anchor/{anchor}/) trusted the client-supplied
`size` value end-to-end: it was stored on the FileAsset and passed straight
to generate_presigned_post(), which uses it as the S3/MinIO policy bound
(["content-length-range", 1, file_size]). This let an authenticated user
obtain a signed upload policy exceeding the instance's FILE_SIZE_LIMIT.
Cap the value with `size_limit = min(size, settings.FILE_SIZE_LIMIT)` and use
it consistently for the stored asset metadata and the presigned POST policy,
matching every other asset upload endpoint.
* fix(api): clamp Space asset size to a valid lower bound
Address review feedback: reject malformed (non-integer) `size` with 400 and
clamp the value to [1, FILE_SIZE_LIMIT] via max(1, min(...)) so the presigned
content-length-range is always valid and no non-positive size is persisted.
* [WEB-7887] fix(security): prevent stored XSS via SVG attachment served 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>
* [WEB-7887] fix: normalize MIME type before SCRIPT_CAPABLE_MIME_TYPES 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>
---------
Co-authored-by: Plane AI <noreply@plane.so>
* [WEB-7776] fix(security): scope FileAsset queries to prevent cross-project IDOR (Cluster F)
Multiple asset endpoints were missing project-level scoping on FileAsset
queryset filters, allowing authenticated users to access, mark-uploaded,
or restore assets belonging to other projects/workspaces.
- ProjectBulkAssetEndpoint.post: add project_id= scope to asset filter
- EntityAssetEndpoint.get/patch: add project_id=deploy_board.project_id
- AssetRestoreEndpoint.post: add project_id=deploy_board.project_id
- FileAssetEndpoint (V1): add workspace membership check on get/post/delete
- FileAssetViewSet.restore (V1): add workspace membership check
- WorkspaceFileAssetEndpoint.post: gate WORKSPACE_LOGO on ADMIN role
- DuplicateAssetEndpoint.post: restrict source asset to same workspace
Fixes GHSA-r2hw, GHSA-jh4v, GHSA-8688, GHSA-3hrj and related advisories.
Co-authored-by: Plane AI <noreply@plane.so>
* refactor(security): replace inline membership checks with WorkspaceMemberPermission class
Add WorkspaceMemberPermission to workspace.py — resolves workspace by
'workspace_id' UUID or 'slug' kwarg, covering the mixed URL patterns on
FileAssetEndpoint. Apply to FileAssetEndpoint and FileAssetViewSet so
membership enforcement lives in the permission layer, not inside each
method handler.
Co-authored-by: Plane AI <noreply@plane.so>
* refactor: remove dead 404 guard in FileAssetEndpoint.post()
WorkspaceMemberPermission denies requests for non-existent slugs before
the view method runs, making the filter().first() + if not workspace
branch unreachable. Switch to .get() so any TOCTOU race still surfaces
as a 404 via ObjectDoesNotExist.
Co-authored-by: Plane AI <noreply@plane.so>
---------
Co-authored-by: Plane AI <noreply@plane.so>
GHSA-6qrq-f73q-r67j / GHSA-j9pv-f5wm-p4g2 — IssueCommentSerializer in
both app and api layers stored comment_html without sanitization. The app
layer had no validate() at all; the api layer only ran lxml structural
normalization which does not strip XSS payloads.
Fix: both serializers now call validate_html_content() (nh3-backed) in
their validate() methods, replacing the raw value with sanitized HTML.
GHSA-hh2r-3hwp-mvq3 — space/views/intake.py and api/views/intake.py
both used bare Issue.objects.create() with description_html taken
directly from request data, bypassing any serializer validation.
Fix: both paths now call validate_html_content() and pass the sanitized
value to Issue.objects.create(). Falls back to "<p></p>" if sanitizer
returns None (empty/invalid input).
The nh3 sanitizer (validate_html_content in content_validator.py) was
already present and used by IssueCreateSerializer — this change extends
coverage to the two remaining unsanitized comment and intake paths.
Co-authored-by: Plane AI <noreply@plane.so>
* fix(api): return HTTP response from dispatch() exception handler
BaseAPIView.dispatch() and BaseViewSet.dispatch() built the proper
error Response via handle_exception() but returned the raw exception
object instead, causing Django to raise
"TypeError: 'Exception' object is not a valid HTTP response".
Fix all six occurrences across the api, app, license and space view
bases, and add a regression test covering every affected base class.
Fixes#9157
* chore(api): add copyright header to tests/unit/views/__init__.py
The empty package init file was missing the AGPL copyright header,
failing the Copy Right Check CI (addlicense -check on all tracked
.py files).
* fix: sanitize filenames in upload paths to prevent path traversal (GHSA-v57h-5999-w7xp)
Add server-side filename sanitization across all file upload endpoints
to prevent path traversal sequences (../) in user-supplied filenames
from being incorporated into S3 object keys. While S3 keys are flat
strings and not vulnerable to filesystem traversal, this adds
defense-in-depth and prevents S3 key pollution.
Changes:
- Add sanitize_filename() utility in path_validator.py
- Sanitize filenames in get_upload_path() for FileAsset and IssueAttachment models
- Sanitize name parameter in all upload view endpoints
* fix: address PR review feedback on filename sanitization
- Remove unused `import re`
- Normalize backslashes to forward slashes before os.path.basename()
so Windows-style paths (e.g. ..\..\..\evil.txt) are handled on POSIX
- Strip whitespace before removing leading dots so " .env" is caught
- Return None instead of "unnamed" for empty input so existing
`if not name` validation guards remain effective
- Add `or "unnamed"` fallback at call sites that lack a name guard
* fix: use random hex name as fallback in get_upload_path instead of "unnamed"
* fix: resolve ruff E501 line too long in DuplicateAssetEndpoint
* chore: traige state in intake
* chore: triage state changes
* feat: implement intake state dropdown component and integrate into issue properties
* chore: added the triage state validation
* chore: added triage state filter
* chore: added workspace filter
* fix: migration file
* chore: added triage group state check
* chore: updated the filters
* chore: updated the filters
* chore: added variables for intake state
* fix: import error
* refactor: improve project intake state retrieval logic and update TriageGroupIcon component
* chore: changed the intake validation logic
* refactor: update intake state types and clean up unused interfaces
* chore: changed the state color
* chore: changed the update serializer
* chore: updated with current instance
* chore: update TriageGroupIcon color to match new intake state group color
* chore: stringified value
* chore: added validation in serializer
* chore: added logger instead of print
* fix: correct component closing syntax in ActiveProjectItem
* chore: updated the migration file
* chore: added noop in migation
---------
Co-authored-by: b-saikrishnakanth <bsaikrishnakanth97@gmail.com>