PT-2026-45488 · Pypi · Praisonai-Platform
Published
2026-06-01
·
Updated
2026-06-01
·
CVE-2026-47417
CVSS v3.1
8.1
High
| Vector | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
Summary
Type: Insecure Direct Object Reference. The comment endpoints (
POST /workspaces/{workspace id}/issues/{issue id}/comments and GET .../comments) gate access on require workspace member(workspace id) only, then call CommentService.create(issue id=issue id, ...) and CommentService.list for issue(issue id) without verifying that issue id belongs to workspace id. A user who is a member of any workspace W1 can read every comment on, and post new comments to, any issue in any other workspace W2.
File: src/praisonai-platform/praisonai platform/api/routes/issues.py, lines 143-171; src/praisonai-platform/praisonai platform/services/comment service.py, lines 19-53.
Root cause: the route extracts workspace id from the URL path and uses it solely for the membership gate, then passes the URL-supplied issue id straight into CommentService without confirming that this issue exists in workspace id. CommentService.list for issue(issue id) runs SELECT * FROM comments WHERE issue id = :issue id with no workspace join. CommentService.create(issue id=issue id, ...) blindly writes a row with that issue id. Both flows trust the URL-supplied issue ID as authoritative even though the membership check guarantees nothing about it.Affected Code
File 1:
src/praisonai-platform/praisonai platform/api/routes/issues.py, lines 143-171.@router.post("/{issue id}/comments", response model=CommentResponse, status code=status.HTTP 201 CREATED)
async def add comment(
workspace id: str,
issue id: str,
body: CommentCreate,
user: AuthIdentity = Depends(require workspace member), # only checks attacker is in workspace id
session: AsyncSession = Depends(get db),
):
svc = CommentService(session)
comment = await svc.create(
issue id=issue id, # <-- BUG: no validation that issue id is in workspace id
author id=user.id,
content=body.content,
author type="member" if user.is user else "agent",
parent id=body.parent id,
)
return CommentResponse.model validate(comment)
@router.get("/{issue id}/comments", response model=List[CommentResponse])
async def list comments(
workspace id: str,
issue id: str,
user: AuthIdentity = Depends(require workspace member),
session: AsyncSession = Depends(get db),
):
svc = CommentService(session)
comments = await svc.list for issue(issue id) # <-- BUG: returns comments on any issue
return [CommentResponse.model validate(c) for c in comments]
File 2:
src/praisonai-platform/praisonai platform/services/comment service.py, lines 19-53.class CommentService:
...
async def create(
self,
issue id: str,
author id: str,
content: str,
author type: str = "member",
comment type: str = "comment",
parent id: Optional[str] = None,
) -> Comment:
comment = Comment(
issue id=issue id, # <-- accepts any issue id; no workspace verify
author type=author type,
author id=author id,
...
)
self. session.add(comment)
await self. session.flush()
return comment
async def list for issue(self, issue id: str) -> list[Comment]:
stmt = (
select(Comment)
.where(Comment.issue id == issue id) # <-- no JOIN against issues for workspace constraint
.order by(Comment.created at)
)
result = await self. session.execute(stmt)
return list(result.scalars().all())
Why it's wrong: the service trusts the caller-supplied
issue id as authoritative, but the route layer never verified that this issue belongs to the workspace the membership check covers. The standard FastAPI/SQLAlchemy fix is to first resolve the issue scoped to workspace id (Issue.id = :issue id AND Issue.workspace id = :workspace id) and only then proceed to comment operations. The MemberService.get(workspace id, user id) and LabelService.list for workspace(workspace id) calls in the same codebase show the safe predicate; the comment service forgot to apply it.Exploit Chain
- Attacker registers a workspace
W attacker(member) and harvests a target issue UUIDI Tfrom any side channel: agent prompts that mention issues, the activity feed (act svc.logrecordsissue id), webhook payloads, exported issue dumps, or simply by being a low-privilege observer of the attacker's own workspace whose internals reference foreign issue IDs (cross-workspace links, search across activity events). State: attacker holdsI T. - Attacker authenticates and sends
GET /workspaces/W attacker/issues/I T/comments.require workspace member(W attacker, attacker)passes (attacker is a member ofW attacker). State: control flow enterslist commentswithworkspace id=W attacker, issue id=I T. CommentService.list for issue(I T)runsSELECT * FROM comments WHERE issue id = 'I T'with no workspace constraint. Every comment on the foreign issue is returned:content(often the most sensitive part of an issue tracker — bug-report repro steps with secrets, customer PII, internal triage notes),author id,author type,parent id,created at. State: response body is the full comment thread of the foreign issue.- Attacker repeats with
POST /workspaces/W attacker/issues/I T/commentsand a body of{"content": "<malicious>"}.CommentService.create(issue id=I T, author id=attacker, ...)writes a row with the foreign issue's id and the attacker'sauthor id. State: a new comment authored by the attacker appears in the foreign workspace's issue thread, indistinguishable to the foreign workspace's UI from a legitimate cross-workspace mention. Used at scale this becomes a comment-spam / phishing primitive (links in the comment body) targeting another tenant's users. - Final state: any attacker with one workspace-member token can exfiltrate every comment in the multi-tenant deployment given the issue UUIDs, and inject arbitrary comments under their own author identity into any foreign issue. The cross-workspace attribution gap is the worst part: the comment is recorded with the attacker's
author id, but the foreign workspace has no member with that id and the foreign workspace's audit logs show no event (theact svc.logcall inadd commentis omitted).
Security Impact
Severity: sec-high. CVSS 7.6: network attack, low complexity, low privileges, no user interaction, scope unchanged, high confidentiality (full comment threads), high integrity (cross-workspace comment injection under attacker's own id), no availability claim.
Attacker capability: read every comment on every issue in the multi-tenant deployment given the issue UUIDs; post arbitrary comments under the attacker's identity into any foreign issue, allowing comment-spam, phishing-link injection into another tenant's UI, or social-engineering attribution attacks (the foreign workspace's UI renders a comment whose author belongs to no member of that workspace).
Preconditions:
praisonai-platform is deployed multi-tenant; the attacker has any membership token; the target issue's UUID is known or guessable.
Differential: source-inspection-verified end-to-end. The asymmetry between CommentService.list for issue(issue id) (no workspace predicate) and LabelService.list for workspace(workspace id) (correctly workspace-scoped) confirms the gap. With the suggested fix below, every comment route first resolves the issue scoped to workspace id, returns 404 if the issue is foreign, and only then proceeds.Suggested Fix
Resolve the issue scoped to
workspace id at the route layer before dispatching to CommentService. This both fixes the read and the write paths and avoids changing the CommentService signature.--- a/src/praisonai-platform/praisonai platform/api/routes/issues.py
+++ b/src/praisonai-platform/praisonai platform/api/routes/issues.py
@@ -141,6 +141,11 @@ async def delete issue(...):
# ── Comments ─────────────────────────────────────────────────────────────────
+async def require issue in workspace(session, workspace id: str, issue id: str):
+ issue = await IssueService(session).get(workspace id, issue id) # workspace-scoped get (see companion advisory)
+ if issue is None:
+ raise HTTPException(status code=404, detail="Issue not found")
+
@router.post("/{issue id}/comments", response model=CommentResponse, status code=status.HTTP 201 CREATED)
async def add comment(
workspace id: str,
@@ -149,6 +154,7 @@ async def add comment(
user: AuthIdentity = Depends(require workspace member),
session: AsyncSession = Depends(get db),
):
+ await require issue in workspace(session, workspace id, issue id)
svc = CommentService(session)
comment = await svc.create(
issue id=issue id,
@@ -167,5 +173,6 @@ async def list comments(
user: AuthIdentity = Depends(require workspace member),
session: AsyncSession = Depends(get db),
):
+ await require issue in workspace(session, workspace id, issue id)
svc = CommentService(session)
comments = await svc.list for issue(issue id)
Companion advisories file the same workspace-scoping gap for
AgentService, IssueService, ProjectService, and LabelService. Each is a separate exploitable IDOR.Fix
IDOR
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Praisonai-Platform