Skip to content

Commit 24385d6

Browse files
Merge pull request #1686 from MervinPraison/security/batch-12-remaining
refactor: security batch 3 — platform service scoping (do not auto-merge)
2 parents 7eb98e8 + 2f81622 commit 24385d6

11 files changed

Lines changed: 128 additions & 57 deletions

File tree

SECURITY_TRIAGE.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@
1010
| GHSA-5cxw-77wg-jrf3 | fixed batch 1 | @url mentions |
1111
| GHSA-xp85-6wwf-r67c | fixed batch 1 | GHA branch quote |
1212
| GHSA-3qg8-5g3r-79v5 | fixed batch 1+2 | JWT + issue guard |
13-
| GHSA-xwq8, 7p8g, c2m8, 8g2p, w388, g8rr | fixed batch 1 | platform RBAC/IDOR partial |
13+
| GHSA-xwq8, 7p8g, w388, g8rr | fixed batch 1 | platform RBAC/IDOR partial |
1414
| GHSA-943m, 5jx9, 4x6r, 27p4, cp4f | fixed batch 2 | platform IDOR completion |
1515
| GHSA-vg22-4gmj-prxw | fixed batch 2 | example eval hardening |
16-
| GHSA-6xj3-927j-6pqw | fixed batch 2 | deploy.py bleach sanitize |
1716
| GHSA-8444-4fhq-fxpq | already-fixed | APIConfig.auth_enabled default True |
1817
| GHSA-78r8-wwqv-r299 | already-fixed | load_user_module gate |
19-
| GHSA-gv23, h8q5, 6h6v, h37g | partial/defer | broader platform audit |
18+
| GHSA-gv23-xxxx-xxxx, GHSA-h8q5-cp56-rr65, GHSA-6h6v-xxxx-xxxx, GHSA-h37g-4h4p-9x97 | fixed batch 3 | service-layer workspace_id on get/update/delete |
19+
| GHSA-c2m8-xxxx-xxxx, GHSA-8g2p-xxxx-xxxx | fixed batch 3 | only owner assigns admin/owner |
20+
| GHSA-h8q5-cp56-rr65 | fixed batch 3 | bind default 127.0.0.1 (+ PLATFORM_HOST) |
21+
| GHSA-6xj3-927j-6pqw | not-applicable | Open WebUI path; not in this repo |
2022
| GHSA-gmjg, 9q28 | published | prior release |
2123

24+
**Code fixed on main; GHSA state still triage until PyPI publish + advisory publish.**
25+
2226
Resources: https://github.com/MervinPraison/PraisonAI · https://docs.praison.ai · https://praison.ai

src/praisonai-platform/praisonai_platform/__main__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@
77
"""
88

99
import argparse
10+
import os
1011
import sys
1112

1213

1314
def main() -> None:
15+
default_host = os.environ.get("PLATFORM_HOST", "127.0.0.1")
1416
parser = argparse.ArgumentParser(description="PraisonAI Platform Server")
15-
parser.add_argument("--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)")
17+
parser.add_argument(
18+
"--host",
19+
default=default_host,
20+
help="Bind host (default: 127.0.0.1, or PLATFORM_HOST env)",
21+
)
1622
parser.add_argument("--port", type=int, default=8000, help="Bind port (default: 8000)")
1723
parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
1824
args = parser.parse_args()

src/praisonai-platform/praisonai_platform/api/routes/agents.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from praisonaiagents.auth import AuthIdentity
1111

12-
from ..deps import ensure_resource_in_workspace, get_db, require_workspace_member
12+
from ..deps import get_db, require_workspace_member
1313
from ..schemas import AgentCreate, AgentResponse, AgentUpdate
1414
from ...services.agent_service import AgentService
1515

@@ -58,10 +58,9 @@ async def get_agent(
5858
session: AsyncSession = Depends(get_db),
5959
):
6060
svc = AgentService(session)
61-
agent = await svc.get(agent_id)
61+
agent = await svc.get(agent_id, workspace_id=workspace_id)
6262
if agent is None:
6363
raise HTTPException(status_code=404, detail="Agent not found")
64-
ensure_resource_in_workspace(agent.workspace_id, workspace_id, label="Agent")
6564
return AgentResponse.model_validate(agent)
6665

6766

@@ -76,6 +75,7 @@ async def update_agent(
7675
svc = AgentService(session)
7776
agent = await svc.update(
7877
agent_id,
78+
workspace_id=workspace_id,
7979
name=body.name,
8080
status=body.status,
8181
instructions=body.instructions,
@@ -85,7 +85,6 @@ async def update_agent(
8585
)
8686
if agent is None:
8787
raise HTTPException(status_code=404, detail="Agent not found")
88-
ensure_resource_in_workspace(agent.workspace_id, workspace_id, label="Agent")
8988
return AgentResponse.model_validate(agent)
9089

9190

@@ -97,10 +96,6 @@ async def delete_agent(
9796
session: AsyncSession = Depends(get_db),
9897
):
9998
svc = AgentService(session)
100-
agent = await svc.get(agent_id)
101-
if agent is None:
102-
raise HTTPException(status_code=404, detail="Agent not found")
103-
ensure_resource_in_workspace(agent.workspace_id, workspace_id, label="Agent")
104-
deleted = await svc.delete(agent_id)
99+
deleted = await svc.delete(agent_id, workspace_id=workspace_id)
105100
if not deleted:
106101
raise HTTPException(status_code=404, detail="Agent not found")

src/praisonai-platform/praisonai_platform/api/routes/dependencies.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ async def delete_dependency(
6262
dep = await svc.get(dep_id)
6363
if dep is None:
6464
raise HTTPException(status_code=404, detail="Dependency not found")
65+
if dep.issue_id != issue_id and dep.depends_on_issue_id != issue_id:
66+
raise HTTPException(status_code=404, detail="Dependency not found")
6567
deleted = await svc.delete(dep_id)
6668
if not deleted:
6769
raise HTTPException(status_code=404, detail="Dependency not found")

src/praisonai-platform/praisonai_platform/api/routes/issues.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,9 @@ async def get_issue(
8787
session: AsyncSession = Depends(get_db),
8888
):
8989
svc = IssueService(session)
90-
issue = await svc.get(issue_id)
90+
issue = await svc.get(issue_id, workspace_id=workspace_id)
9191
if issue is None:
9292
raise HTTPException(status_code=404, detail="Issue not found")
93-
ensure_resource_in_workspace(issue.workspace_id, workspace_id, label="Issue")
9493
return IssueResponse.model_validate(issue)
9594

9695

@@ -105,6 +104,7 @@ async def update_issue(
105104
svc = IssueService(session)
106105
issue = await svc.update(
107106
issue_id,
107+
workspace_id=workspace_id,
108108
title=body.title,
109109
description=body.description,
110110
status=body.status,
@@ -115,7 +115,6 @@ async def update_issue(
115115
)
116116
if issue is None:
117117
raise HTTPException(status_code=404, detail="Issue not found")
118-
ensure_resource_in_workspace(issue.workspace_id, workspace_id, label="Issue")
119118
act_svc = ActivityService(session)
120119
await act_svc.log(
121120
workspace_id, "issue.updated", "issue", issue.id,
@@ -134,11 +133,7 @@ async def delete_issue(
134133
session: AsyncSession = Depends(get_db),
135134
):
136135
svc = IssueService(session)
137-
issue = await svc.get(issue_id)
138-
if issue is None:
139-
raise HTTPException(status_code=404, detail="Issue not found")
140-
ensure_resource_in_workspace(issue.workspace_id, workspace_id, label="Issue")
141-
deleted = await svc.delete(issue_id)
136+
deleted = await svc.delete(issue_id, workspace_id=workspace_id)
142137
if not deleted:
143138
raise HTTPException(status_code=404, detail="Issue not found")
144139

src/praisonai-platform/praisonai_platform/api/routes/projects.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from praisonaiagents.auth import AuthIdentity
1111

12-
from ..deps import ensure_resource_in_workspace, get_db, require_workspace_member
12+
from ..deps import get_db, require_workspace_member
1313
from ..schemas import ProjectCreate, ProjectResponse, ProjectUpdate
1414
from ...services.project_service import ProjectService
1515

@@ -56,10 +56,9 @@ async def get_project(
5656
session: AsyncSession = Depends(get_db),
5757
):
5858
svc = ProjectService(session)
59-
project = await svc.get(project_id)
59+
project = await svc.get(project_id, workspace_id=workspace_id)
6060
if project is None:
6161
raise HTTPException(status_code=404, detail="Project not found")
62-
ensure_resource_in_workspace(project.workspace_id, workspace_id, label="Project")
6362
return ProjectResponse.model_validate(project)
6463

6564

@@ -74,6 +73,7 @@ async def update_project(
7473
svc = ProjectService(session)
7574
project = await svc.update(
7675
project_id,
76+
workspace_id=workspace_id,
7777
title=body.title,
7878
description=body.description,
7979
status=body.status,
@@ -82,7 +82,6 @@ async def update_project(
8282
)
8383
if project is None:
8484
raise HTTPException(status_code=404, detail="Project not found")
85-
ensure_resource_in_workspace(project.workspace_id, workspace_id, label="Project")
8685
return ProjectResponse.model_validate(project)
8786

8887

@@ -94,11 +93,7 @@ async def delete_project(
9493
session: AsyncSession = Depends(get_db),
9594
):
9695
svc = ProjectService(session)
97-
project = await svc.get(project_id)
98-
if project is None:
99-
raise HTTPException(status_code=404, detail="Project not found")
100-
ensure_resource_in_workspace(project.workspace_id, workspace_id, label="Project")
101-
deleted = await svc.delete(project_id)
96+
deleted = await svc.delete(project_id, workspace_id=workspace_id)
10297
if not deleted:
10398
raise HTTPException(status_code=404, detail="Project not found")
10499

@@ -111,8 +106,7 @@ async def project_stats(
111106
session: AsyncSession = Depends(get_db),
112107
):
113108
svc = ProjectService(session)
114-
project = await svc.get(project_id)
109+
project = await svc.get(project_id, workspace_id=workspace_id)
115110
if project is None:
116111
raise HTTPException(status_code=404, detail="Project not found")
117-
ensure_resource_in_workspace(project.workspace_id, workspace_id, label="Project")
118112
return await svc.get_stats(project_id)

src/praisonai-platform/praisonai_platform/api/routes/workspaces.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@ async def add_member(
103103
session: AsyncSession = Depends(get_db),
104104
):
105105
member_svc = MemberService(session)
106-
if body.role == "owner":
106+
if body.role in ("owner", "admin"):
107107
if not await member_svc.has_role(workspace_id, user.id, "owner"):
108108
raise HTTPException(
109109
status_code=status.HTTP_403_FORBIDDEN,
110-
detail="Only owners can add another owner",
110+
detail="Only owners can add admin or owner roles",
111111
)
112112
member = await member_svc.add(workspace_id, body.user_id, body.role)
113113
return MemberResponse.model_validate(member)
@@ -141,12 +141,12 @@ async def update_member_role(
141141
status_code=status.HTTP_403_FORBIDDEN,
142142
detail="Cannot change your own role",
143143
)
144-
if body.role == "owner" and not await member_svc.has_role(
144+
if body.role in ("owner", "admin") and not await member_svc.has_role(
145145
workspace_id, user.id, "owner"
146146
):
147147
raise HTTPException(
148148
status_code=status.HTTP_403_FORBIDDEN,
149-
detail="Only owners can assign the owner role",
149+
detail="Only owners can assign admin or owner roles",
150150
)
151151
if target.role == "owner" and not await member_svc.has_role(
152152
workspace_id, user.id, "owner"

src/praisonai-platform/praisonai_platform/services/agent_service.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,16 @@ async def create(
5050
await self._session.flush()
5151
return agent
5252

53-
async def get(self, agent_id: str) -> Optional[Agent]:
54-
"""Get agent by ID."""
55-
return await self._session.get(Agent, agent_id)
53+
async def get(
54+
self, agent_id: str, *, workspace_id: Optional[str] = None
55+
) -> Optional[Agent]:
56+
"""Get agent by ID, optionally scoped to a workspace."""
57+
agent = await self._session.get(Agent, agent_id)
58+
if agent is None:
59+
return None
60+
if workspace_id is not None and agent.workspace_id != workspace_id:
61+
return None
62+
return agent
5663

5764
async def list_for_workspace(
5865
self,
@@ -72,6 +79,8 @@ async def list_for_workspace(
7279
async def update(
7380
self,
7481
agent_id: str,
82+
*,
83+
workspace_id: Optional[str] = None,
7584
name: Optional[str] = None,
7685
status: Optional[str] = None,
7786
instructions: Optional[str] = None,
@@ -80,7 +89,7 @@ async def update(
8089
max_concurrent_tasks: Optional[int] = None,
8190
) -> Optional[Agent]:
8291
"""Update agent fields."""
83-
agent = await self.get(agent_id)
92+
agent = await self.get(agent_id, workspace_id=workspace_id)
8493
if agent is None:
8594
return None
8695
if name is not None:
@@ -102,9 +111,11 @@ async def update(
102111
await self._session.flush()
103112
return agent
104113

105-
async def delete(self, agent_id: str) -> bool:
114+
async def delete(
115+
self, agent_id: str, *, workspace_id: Optional[str] = None
116+
) -> bool:
106117
"""Delete an agent."""
107-
agent = await self.get(agent_id)
118+
agent = await self.get(agent_id, workspace_id=workspace_id)
108119
if agent is None:
109120
return False
110121
await self._session.delete(agent)

src/praisonai-platform/praisonai_platform/services/issue_service.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,16 @@ async def create(
6969
await self._session.flush()
7070
return issue
7171

72-
async def get(self, issue_id: str) -> Optional[Issue]:
73-
"""Get issue by ID."""
74-
return await self._session.get(Issue, issue_id)
72+
async def get(
73+
self, issue_id: str, *, workspace_id: Optional[str] = None
74+
) -> Optional[Issue]:
75+
"""Get issue by ID, optionally scoped to a workspace."""
76+
issue = await self._session.get(Issue, issue_id)
77+
if issue is None:
78+
return None
79+
if workspace_id is not None and issue.workspace_id != workspace_id:
80+
return None
81+
return issue
7582

7683
async def list_for_workspace(
7784
self,
@@ -97,6 +104,8 @@ async def list_for_workspace(
97104
async def update(
98105
self,
99106
issue_id: str,
107+
*,
108+
workspace_id: Optional[str] = None,
100109
title: Optional[str] = None,
101110
description: Optional[str] = None,
102111
status: Optional[str] = None,
@@ -106,7 +115,7 @@ async def update(
106115
project_id: Optional[str] = None,
107116
) -> Optional[Issue]:
108117
"""Update issue fields."""
109-
issue = await self.get(issue_id)
118+
issue = await self.get(issue_id, workspace_id=workspace_id)
110119
if issue is None:
111120
return None
112121
if title is not None:
@@ -135,21 +144,27 @@ async def assign(
135144
issue_id: str,
136145
assignee_type: str,
137146
assignee_id: str,
147+
*,
148+
workspace_id: Optional[str] = None,
138149
) -> Optional[Issue]:
139150
"""Assign an issue to a member or agent."""
140151
if assignee_type not in VALID_ASSIGNEE_TYPES:
141152
raise ValueError(f"Invalid assignee_type: {assignee_type}")
142153
return await self.update(
143-
issue_id, assignee_type=assignee_type, assignee_id=assignee_id
154+
issue_id, workspace_id=workspace_id, assignee_type=assignee_type, assignee_id=assignee_id
144155
)
145156

146-
async def transition(self, issue_id: str, new_status: str) -> Optional[Issue]:
157+
async def transition(
158+
self, issue_id: str, new_status: str, *, workspace_id: Optional[str] = None
159+
) -> Optional[Issue]:
147160
"""Transition an issue to a new status."""
148-
return await self.update(issue_id, status=new_status)
161+
return await self.update(issue_id, workspace_id=workspace_id, status=new_status)
149162

150-
async def delete(self, issue_id: str) -> bool:
163+
async def delete(
164+
self, issue_id: str, *, workspace_id: Optional[str] = None
165+
) -> bool:
151166
"""Delete an issue."""
152-
issue = await self.get(issue_id)
167+
issue = await self.get(issue_id, workspace_id=workspace_id)
153168
if issue is None:
154169
return False
155170
await self._session.delete(issue)

0 commit comments

Comments
 (0)