praisonai-platform: list_issue_activity returns activity log for any issue regardless of workspace ownership
Description
Summary
Type: Insecure Direct Object Reference. The GET /workspaces/{workspace_id}/issues/{issue_id}/activity endpoint is gated by require_workspace_member(workspace_id) and dispatches to ActivityService.list_for_issue(issue_id), which executes SELECT * FROM activity WHERE issue_id = :issue_id with no workspace constraint. A user who is a member of any workspace can read the full activity log of any issue across the entire multi-tenant deployment. File: src/praisonai-platform/praisonai_platform/api/routes/activity.py, lines 32-43; services/activity_service.py's list_for_issue method.
Root cause: the route extracts workspace_id from the URL path, uses it solely for the membership gate, then passes the URL-supplied issue_id directly to ActivityService.list_for_issue(issue_id) without verifying which workspace the issue belongs to. The companion list_workspace_activity endpoint at line 19-29 is implemented correctly (it passes workspace_id to svc.list_for_workspace(workspace_id)) — the asymmetry is the smoking gun.
Affected
Code
File: src/praisonai-platform/praisonai_platform/api/routes/activity.py, lines 19-43.
@router.get("/activity", response_model=List[ActivityLogResponse])
async def list_workspace_activity(
workspace_id: str,
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
user: AuthIdentity = Depends(require_workspace_member),
session: AsyncSession = Depends(get_db),
):
svc = ActivityService(session)
logs = await svc.list_for_workspace(workspace_id, limit=limit, offset=offset) # correct: passes workspace_id
return [ActivityLogResponse.model_validate(log) for log in logs]
@router.get("/issues/{issue_id}/activity", response_model=List[ActivityLogResponse])
async def list_issue_activity(
workspace_id: str,
issue_id: str,
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
user: AuthIdentity = Depends(require_workspace_member),
session: AsyncSession = Depends(get_db),
):
svc = ActivityService(session)
logs = await svc.list_for_issue(issue_id, limit=limit, offset=offset) # <-- BUG: no workspace_id
return [ActivityLogResponse.model_validate(log) for log in logs]
Why it's wrong: activity logs are typically the most sensitive operational record — they include actor identity, action type, entity references, and a free-form details JSON blob that may contain pre-/post-change values for any tracked field. Reading the foreign workspace's activity log gives the attacker a high-fidelity view into who did what when, which is gold for further reconnaissance (cross-workspace member enumeration, foreign issue title disclosure, knowing which projects exist). The same author got list_workspace_activity right by passing workspace_id — the issue-scoped variant is the gap.
Exploit
Chain
- Attacker is a member of workspace
W_attackerand harvests a target issue UUIDI_Tfrom any side channel. State: attacker holdsI_T. - Attacker sends
GET /workspaces/W_attacker/issues/I_T/activity?limit=200withAuthorization: Bearer <attacker_jwt>. State: control flow enterslist_issue_activity. require_workspace_member(W_attacker, attacker)passes.ActivityService.list_for_issue(I_T)runsSELECT * FROM activity WHERE issue_id = 'I_T' ORDER BY created_at DESC LIMIT 200. State: response body is the full activity log for the foreign issue.- The activity entries reveal: every actor (member or agent) who touched the issue, every action (created, updated, commented, status_changed, assignee_changed, project_changed, label_added, dependency_added), and the
detailsJSON blob containing the before/after values of every change. State: the attacker fingerprints the foreign workspace's triage workflow, identifies who works on what, and sees the issue's complete history including any embedded secrets that ever passed through the description or comments. - Final state: with one workspace-member token plus one GET, the attacker reads the full activity timeline of any issue in the multi-tenant deployment given the issue UUIDs.
Security
Impact
Severity: sec-moderate. CVSS 6.5: network attack, low complexity, low privileges, no user interaction, scope unchanged, high confidentiality (full activity log including before/after details), no integrity claim (read-only), no availability claim.
Attacker capability: read the activity log of any issue in the deployment given its UUID. Combined with the companion issue-IDOR (which already gives full issue content), this is recon for the foreign workspace's operational tempo, member identity, and triage workflow.
Preconditions: praisonai-platform is deployed multi-tenant; attacker has any workspace-membership token; foreign issue UUIDs are reachable.
Differential: source-inspection-verified. The asymmetry between list_workspace_activity (correctly workspace-scoped) and list_issue_activity (no workspace check) confirms the gap. With the suggested fix below, the route first resolves the issue via IssueService.get(workspace_id, issue_id), returns 404 for foreign issues, and only then proceeds.
Suggested
Fix
--- a/src/praisonai-platform/praisonai_platform/api/routes/activity.py
+++ b/src/praisonai-platform/praisonai_platform/api/routes/activity.py
@@ -32,9 +32,12 @@
@router.get("/issues/{issue_id}/activity", response_model=List[ActivityLogResponse])
async def list_issue_activity(
workspace_id: str,
issue_id: str,
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
user: AuthIdentity = Depends(require_workspace_member),
session: AsyncSession = Depends(get_db),
):
+ issue_svc = IssueService(session)
+ if await issue_svc.get(workspace_id, issue_id) is None: # workspace-scoped get from issue-IDOR companion
+ raise HTTPException(status_code=404, detail="Issue not found")
svc = ActivityService(session)
logs = await svc.list_for_issue(issue_id, limit=limit, offset=offset)
return [ActivityLogResponse.model_validate(log) for log in logs]
The same single-key issue lookup pattern is filed separately as the IssueService IDOR; once that is fixed, the helper used here is just IssueService.get(workspace_id, issue_id).
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
An IDOR vulnerability in PraisonAI Platform allows workspace members to read activity logs of any issue across tenants due to missing workspace constraint.
Vulnerability
The GET /workspaces/{workspace_id}/issues/{issue_id}/activity endpoint in src/praisonai-platform/praisonai_platform/api/routes/activity.py performs a workspace membership check via require_workspace_member(workspace_id), but then calls ActivityService.list_for_issue(issue_id) without verifying that the issue belongs to the specified workspace. The service method executes SELECT * FROM activity WHERE issue_id = :issue_id with no workspace constraint, allowing an authenticated user who is a member of any workspace to read the activity log of any issue across the multi-tenant deployment [1][2]. The companion endpoint list_workspace_activity correctly passes workspace_id to the service, highlighting the oversight.
Exploitation
An attacker must be an authenticated user and a member of at least one workspace in the PraisonAI Platform. To exploit, the attacker sends a GET request to /workspaces/<victim_workspace_id>/issues/<target_issue_id>/activity with a valid session token. The server checks workspace membership for the ID in the URL, which the attacker can supply arbitrarily, and then fetches activity records solely by issue_id without any workspace ownership validation [1][2]. No additional privileges or user interaction are required.
Impact
Successful exploitation results in unauthorized disclosure of the full activity log of any issue in any workspace. The activity log includes sensitive information about issue changes, comments, and timestamps. This violates tenant isolation in the multi-tenant architecture and can lead to information leakage across workspaces [1][2].
Mitigation
As of the publication date of the advisory, no patch version has been released [1][2]. The fix should modify ActivityService.list_for_issue to accept and enforce a workspace_id parameter, ensuring that only activity records belonging to the workspace are returned. Until a patch is available, administrators may consider network-level restrictions or disabling the endpoint.
AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
praisonai-platformPyPI | < 0.1.4 | 0.1.4 |
Affected products
1Patches
18b72cec167ecMerge pull request #168 from MervinPraison/develop
6 files changed · +25 −18
Dockerfile+1 −1 modified@@ -1,6 +1,6 @@ FROM python:3.11-slim WORKDIR /app COPY . . -RUN pip install flask praisonai==0.1.2 gunicorn markdown +RUN pip install flask praisonai==0.1.3 gunicorn markdown EXPOSE 8080 CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]
docs/api/praisonai/deploy.html+1 −1 modified@@ -110,7 +110,7 @@ <h2 id="raises">Raises</h2> file.write("FROM python:3.11-slim\n") file.write("WORKDIR /app\n") file.write("COPY . .\n") - file.write("RUN pip install flask praisonai==0.1.2 gunicorn markdown\n") + file.write("RUN pip install flask praisonai==0.1.3 gunicorn markdown\n") file.write("EXPOSE 8080\n") file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
praisonai/deploy.py+1 −1 modified@@ -56,7 +56,7 @@ def create_dockerfile(self): file.write("FROM python:3.11-slim\n") file.write("WORKDIR /app\n") file.write("COPY . .\n") - file.write("RUN pip install flask praisonai==0.1.2 gunicorn markdown\n") + file.write("RUN pip install flask praisonai==0.1.3 gunicorn markdown\n") file.write("EXPOSE 8080\n") file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
praisonai.rb+1 −1 modified@@ -3,7 +3,7 @@ class Praisonai < Formula desc "AI tools for various AI applications" homepage "https://github.com/MervinPraison/PraisonAI" - url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/0.1.2.tar.gz" + url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/0.1.3.tar.gz" sha256 "1828fb9227d10f991522c3f24f061943a254b667196b40b1a3e4a54a8d30ce32" # Replace with actual SHA256 checksum license "MIT"
praisonai/ui/realtime.py+20 −13 modified@@ -6,7 +6,6 @@ from openai import AsyncOpenAI import chainlit as cl -from chainlit.logger import logger from chainlit.input_widget import TextInput from chainlit.types import ThreadDict @@ -16,6 +15,25 @@ import chainlit.data as cl_data from literalai.helper import utc_now import json +import logging +import importlib.util +from importlib import import_module +from pathlib import Path + +# Set up logging +logger = logging.getLogger(__name__) +log_level = os.getenv("LOGLEVEL", "INFO").upper() +logger.handlers = [] + +# Set up logging to console +console_handler = logging.StreamHandler() +console_handler.setLevel(log_level) +console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +console_handler.setFormatter(console_formatter) +logger.addHandler(console_handler) + +# Set the logging level for the logger +logger.setLevel(log_level) # Set up CHAINLIT_AUTH_SECRET CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET") @@ -144,19 +162,8 @@ def load_setting(key: str) -> str: client = AsyncOpenAI() -# Add these new imports and code -import importlib.util -import logging -from importlib import import_module -from pathlib import Path - -# Set up logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - # Try to import tools from the root directory -root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -tools_path = os.path.join(root_dir, 'tools.py') +tools_path = os.path.join(os.getcwd(), 'tools.py') logger.info(f"Tools path: {tools_path}") def import_tools_from_file(file_path):
pyproject.toml+1 −1 modified@@ -1,6 +1,6 @@ [tool.poetry] name = "PraisonAI" -version = "0.1.2" +version = "0.1.3" description = "PraisonAI application combines AutoGen and CrewAI or similar frameworks into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customization, and efficient human-agent collaboration." authors = ["Mervin Praison"] license = ""
Vulnerability mechanics
Root cause
"The `list_issue_activity` endpoint uses the URL-supplied `workspace_id` only for the membership gate and passes `issue_id` directly to `ActivityService.list_for_issue` without verifying which workspace the issue belongs to."
Attack vector
An attacker who is a member of any workspace can send `GET /workspaces/{workspace_id}/issues/{issue_id}/activity` with a valid bearer token and a foreign issue UUID. The `require_workspace_member` gate only checks workspace membership, not issue ownership, so `ActivityService.list_for_issue(issue_id)` executes a query with no workspace constraint [ref_id=1][ref_id=2]. This returns the full activity log of the foreign issue, including actor identity, action type, and a `details` JSON blob with before/after values of every tracked field.
Affected code
The vulnerability is in `src/praisonai-platform/praisonai_platform/api/routes/activity.py`, lines 32-43, in the `list_issue_activity` endpoint. The companion `list_workspace_activity` endpoint at lines 19-29 is implemented correctly, highlighting the asymmetry.
What the fix does
The patch is not present in the supplied bundle; the advisory suggests adding a workspace-scoped issue lookup before the activity query [ref_id=1][ref_id=2]. Specifically, the route should call `IssueService.get(workspace_id, issue_id)` and raise a 404 if the issue does not belong to the caller's workspace. This ensures that even if the attacker passes a valid workspace_id and issue_id, the activity log is only returned when the issue is owned by that workspace.
Preconditions
- configThe praisonai-platform must be deployed in a multi-tenant configuration.
- authThe attacker must possess a valid workspace-membership token (any workspace).
- inputThe attacker must know or guess a foreign issue UUID.
Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.