GHSA-cp4f-5m9r-5jc2
HIGHGHSA-cp4f-5m9r-5jc2 is a high-severity (CVSS 8.1) CWE-639 vulnerability in praisonai-platform. O3 Security confirms whether GHSA-cp4f-5m9r-5jc2 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
praisonai-platform: Comment endpoints accept any issue_id without workspace ownership check, cross-workspace comment read and post IDOR
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for GHSA-cp4f-5m9r-5jc2.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-cp4f-5m9r-5jc2 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 366,298 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
praisonai-platformReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | praisonai-platform | all versions | 0.1.4 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for praisonai-platform. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update praisonai-platform to 0.1.4 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-cp4f-5m9r-5jc2 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-cp4f-5m9r-5jc2 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-cp4f-5m9r-5jc2. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-cp4f-5m9r-5jc2 in your dependencies?
O3 detects GHSA-cp4f-5m9r-5jc2 across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.