Severity: Medium
File: backend/src/routes/projects.ts:124-130
CWE: CWE-284 — Improper Access Control
OWASP: A01:2021 — Broken Access Control
Description
GET /projects/:projectId uses a case-sensitive Array.includes() check to verify project membership, while checkProjectAccess() (used everywhere else) performs a case-insensitive comparison:
// projects.ts:124 — case-SENSITIVE (bug):
const canAccess = project.user_id === userId ||
(userEmail && Array.isArray(project.shared_with) &&
project.shared_with.includes(userEmail)); // ← case-sensitive
// access.ts — case-INSENSITIVE (correct):
if (email && sharedWith.some((e) => (e ?? "").toLowerCase() === email)) {
Additionally, POST /projects stores shared_with without normalizing email casing, so mixed-case values can be written to the database.
Impact
A legitimate shared user whose email was stored with different casing (e.g., invited as "Alice@Company.Com" but authenticating as "alice@company.com") is incorrectly denied access on this specific GET endpoint. This is an access denial bug, not an access elevation — it does not grant unauthorized access.
Fix
1. Normalize shared_with on project creation (POST /projects):
const seen = new Set<string>();
const normalizedSharedWith: string[] = [];
for (const raw of Array.isArray(shared_with) ? shared_with : []) {
if (typeof raw !== "string") continue;
const e = raw.trim().toLowerCase();
if (!e || seen.has(e)) continue;
seen.add(e);
normalizedSharedWith.push(e);
}
2. Replace the inline includes() check with checkProjectAccess() in GET /projects/:projectId:
const access = await checkProjectAccess(projectId, userId, userEmail, db);
if (!access.ok)
return void res.status(404).json({ detail: "Project not found" });
Remediation tier: Backlog — schedule within 90 days.
Severity: Medium
File:
backend/src/routes/projects.ts:124-130CWE: CWE-284 — Improper Access Control
OWASP: A01:2021 — Broken Access Control
Description
GET /projects/:projectIduses a case-sensitiveArray.includes()check to verify project membership, whilecheckProjectAccess()(used everywhere else) performs a case-insensitive comparison:Additionally,
POST /projectsstoresshared_withwithout normalizing email casing, so mixed-case values can be written to the database.Impact
A legitimate shared user whose email was stored with different casing (e.g., invited as
"Alice@Company.Com"but authenticating as"alice@company.com") is incorrectly denied access on this specific GET endpoint. This is an access denial bug, not an access elevation — it does not grant unauthorized access.Fix
1. Normalize
shared_withon project creation (POST /projects):2. Replace the inline
includes()check withcheckProjectAccess()inGET /projects/:projectId:Remediation tier: Backlog — schedule within 90 days.