Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions .github/workflows/claude-code-review.yml

This file was deleted.

90 changes: 0 additions & 90 deletions .github/workflows/openai-code-review.yml

This file was deleted.

30 changes: 30 additions & 0 deletions backend/src/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Database from "better-sqlite3";
import path from "path";

const dbPath = path.join(__dirname, "..", "teamexpense.db");
const db = new Database(dbPath);

db.pragma("journal_mode = WAL");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enable foreign key enforcement in SQLite.

SQLite does not enforce foreign key constraints by default. The FOREIGN KEY clause on line 26 will be silently ignored without enabling enforcement, allowing orphaned expense records if users are deleted.

🔧 Proposed fix to enable foreign key enforcement
 db.pragma("journal_mode = WAL");
+db.pragma("foreign_keys = ON");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
db.pragma("journal_mode = WAL");
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/db.ts` at line 7, SQLite does not enforce FOREIGN KEY constraints
by default, so after the existing db.pragma("journal_mode = WAL"); call add
db.pragma("foreign_keys = ON"); (or equivalent statement executed immediately
after opening the DB) to enable enforcement before any schema/table creation or
mutations; ensure this is applied in the same initialization flow where the db
variable is created so the FOREIGN KEY clauses in the schema (the expense
table's FOREIGN KEY) are honored.


db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member'
);

CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
amount REAL NOT NULL,
description TEXT NOT NULL,
category TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
Comment on lines +18 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding indexes for query performance.

The expenses table will be frequently queried by user_id (to list a user's expenses) and potentially filtered by status. Without indexes, these queries will require full table scans as data grows.

📈 Proposed schema additions for indexing
     FOREIGN KEY (user_id) REFERENCES users(id)
   );
+
+  CREATE INDEX IF NOT EXISTS idx_expenses_user_id ON expenses(user_id);
+  CREATE INDEX IF NOT EXISTS idx_expenses_status ON expenses(status);
 `);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
amount REAL NOT NULL,
description TEXT NOT NULL,
category TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
amount REAL NOT NULL,
description TEXT NOT NULL,
category TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX IF NOT EXISTS idx_expenses_user_id ON expenses(user_id);
CREATE INDEX IF NOT EXISTS idx_expenses_status ON expenses(status);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/db.ts` around lines 18 - 27, The schema for the expenses table
lacks indexes which will cause slow queries over time; add indexes on the
frequently queried columns by creating indexes such as an index on
expenses(user_id) and another on expenses(status) (optionally a composite index
like expenses(user_id, status)) so queries that filter by user_id and/or status
use the indexes; update the migration that defines the expenses table (look for
the CREATE TABLE IF NOT EXISTS expenses block) to include corresponding CREATE
INDEX statements after the table creation.

`);

export default db;
30 changes: 30 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import express from "express";
import cors from "cors";
import authRoutes from "./routes/auth";
import expenseRoutes from "./routes/expenses";
import reportRoutes from "./routes/reports";

const app = express();
const PORT = process.env.PORT || 3001;

// BUG: Wildcard CORS — allows any origin to make authenticated requests
app.use(cors());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restrict CORS to an explicit allowlist.

app.use(cors()) allows any origin. For authenticated APIs, this is overly permissive and expands cross-origin abuse risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/index.ts` at line 11, Current use of app.use(cors()) allows any
origin; replace it with a scoped CORS policy by creating an allowlist (e.g.,
allowedOrigins array) and configuring cors with an origin validator function or
options object, then register it with app.use(cors(corsOptions)). Update the
code around the existing app.use and import of cors so the origin function
checks incoming Origin against allowedOrigins and either calls back with an
error or allows the request; ensure you still pass options for credentials if
needed (e.g., credentials: true) so Auth flows continue to work.

app.use(express.json());

app.use("/api/auth", authRoutes);
app.use("/api/expenses", expenseRoutes);
app.use("/api/reports", reportRoutes);

// BUG: No rate limiting on any endpoints — susceptible to brute-force attacks
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add request rate limiting on auth and data endpoints.

No throttling leaves login and API routes open to brute-force and scraping abuse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/index.ts` at line 18, Add request rate limiting middleware for
authentication and data/API routes to prevent brute-force and scraping: install
and import a rate limiter (e.g., express-rate-limit), create two limiter
instances (authLimiter with tight limits like 5-10 requests per minute and
longer window/penalty; apiLimiter with higher but bounded rate like 100-1000
requests per hour), and register them on the Express app before the route
handlers (apply authLimiter to the auth router/paths such as the login/logout
handlers and apply apiLimiter to data/API routers/endpoints). Also ensure
app.set('trust proxy', true) if behind a proxy so limiter uses correct client
IP, and return proper Retry-After headers and JSON error responses from the
limiter configuration.


// BUG: Global error handler leaks stack traces to the client in production
app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(err);
res.status(500).json({ error: err.message, stack: err.stack });
});
Comment on lines +21 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not return stack traces in production responses.

Returning { error, stack } leaks internals and can aid exploitation. Keep detailed stacks in server logs only.

Proposed fix
 app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
   console.error(err);
-  res.status(500).json({ error: err.message, stack: err.stack });
+  res.status(500).json({ error: "Internal server error" });
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(err);
res.status(500).json({ error: err.message, stack: err.stack });
});
app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(err);
res.status(500).json({ error: "Internal server error" });
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/index.ts` around lines 21 - 24, The error-handling middleware
currently returns full error stack to clients; change the middleware (the
app.use error handler function) to always log the full error/server-side details
(console.error(err)) but only send a minimal response to clients: in production
(process.env.NODE_ENV === 'production') return res.status(500).json({ error:
err.message }) without err.stack, otherwise (non-production) you may include
stack for debugging. Ensure the change is applied to the anonymous error handler
passed to app.use so no stack traces are exposed in production responses.


app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

export default app;
36 changes: 36 additions & 0 deletions backend/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";

const JWT_SECRET = "supersecretkey123"; // BUG: Hardcoded JWT secret — should use environment variable
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove hardcoded JWT secret from source code.

Embedding the secret in code is a critical security exposure and makes token forgery materially easier if code is leaked.

Proposed fix
-const JWT_SECRET = "supersecretkey123";
+const JWT_SECRET = process.env.JWT_SECRET;
+if (!JWT_SECRET) {
+  throw new Error("JWT_SECRET is not configured");
+}
As per coding guidelines, `backend/src/middleware/**`: Middleware: Check JWT validation logic, error handling, and ensure auth bypass is not possible. Verify token expiry handling.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/auth.ts` at line 4, The file currently hardcodes
JWT_SECRET ("supersecretkey123"); replace that with reading from an environment
variable (e.g., process.env.JWT_SECRET) and fail fast if it's missing so no
default secret is used; update the middleware that uses JWT_SECRET (look for
identifiers like JWT_SECRET and the request handler functions authenticate,
verifyToken, or similar in backend/src/middleware/auth.ts) to use the
env-derived secret for jwt.verify, ensure verification checks token expiry and
returns/throws an unauthorized error (401) on any verification error, and add a
clear startup/log error if the env var is not set so the server will not run
with an insecure fallback.


export interface AuthRequest extends Request {
user?: { id: number; email: string; role: string };
}

export function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header) {
return res.status(401).json({ error: "No token provided" });
}

// BUG: Does not validate "Bearer " prefix — any string with a valid JWT payload passes
const token = header.replace("Bearer ", "");

Comment on lines +16 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate Authorization header format strictly.

Using replace("Bearer ", "") accepts malformed headers. Enforce exact Bearer <token> format before verification.

Proposed fix
-  const token = header.replace("Bearer ", "");
+  const [scheme, token] = header.split(" ");
+  if (scheme !== "Bearer" || !token) {
+    return res.status(401).json({ error: "Invalid authorization header format" });
+  }
As per coding guidelines, `backend/src/middleware/**`: Middleware: Check JWT validation logic, error handling, and ensure auth bypass is not possible. Verify token expiry handling.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// BUG: Does not validate "Bearer " prefix — any string with a valid JWT payload passes
const token = header.replace("Bearer ", "");
// BUG: Does not validate "Bearer " prefix — any string with a valid JWT payload passes
const [scheme, token] = header.split(" ");
if (scheme !== "Bearer" || !token) {
return res.status(401).json({ error: "Invalid authorization header format" });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/auth.ts` around lines 16 - 18, The Authorization
header extraction currently uses header.replace("Bearer ", "") which accepts
malformed values; change the logic in backend/src/middleware/auth.ts to first
ensure header is a string that starts exactly with "Bearer " (e.g.,
header.startsWith("Bearer ")), reject otherwise with a 401/unauthorized
response, then extract the token via splitting (ensure the token part is
non-empty) before calling the JWT verification routine (the token const and the
verify/validate function you use); also propagate verification errors (including
expiration) to return proper 401 responses and avoid any auth bypass.

try {
const payload = jwt.verify(token, JWT_SECRET) as any;
req.user = payload;
next();
} catch {
return res.status(401).json({ error: "Invalid token" });
}
Comment on lines +10 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's explore the directory structure to locate auth/token issuance code
echo "=== Locating auth-related files in backend/src/routes ==="
fd auth backend/src/routes -t f 2>/dev/null || echo "No auth files found in backend/src/routes"

echo -e "\n=== Searching for jwt.sign calls across backend/src/routes ==="
rg -nP 'jwt\.sign\s*\(' backend/src/routes -t ts -t js -A5 2>/dev/null || echo "No jwt.sign found"

echo -e "\n=== Listing all files in backend/src/routes ==="
fd . backend/src/routes -t f 2>/dev/null | head -20

echo -e "\n=== Searching for jwt.sign across entire backend/src ==="
rg -nP 'jwt\.sign\s*\(' backend/src -t ts -t js -A5 2>/dev/null || echo "No jwt.sign found"

echo -e "\n=== Searching for expiresIn or exp settings ==="
rg -nP 'expiresIn|exp\s*:' backend/src -t ts -t js -B2 -A2 2>/dev/null || echo "No expiry settings found"

Repository: reeder32/teamexpense

Length of output: 1849


Add expiresIn to all JWT issuance calls—tokens are currently issued without expiration.

Tokens issued in backend/src/routes/auth.ts (lines 29 and 65) do not include an expiresIn parameter, allowing JWTs to remain valid indefinitely. This creates a critical security gap: compromised tokens can never expire, and users cannot effectively force logout. Add an expiresIn value (e.g., "24h" or "7d") to both jwt.sign() calls. The authenticate middleware will respect expiry if configured, but issuance must enforce it. This aligns with the coding guideline requirement to verify token expiry handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/auth.ts` around lines 10 - 25, The JWTs issued in
backend/src/routes/auth.ts are missing expiration—update both jwt.sign(...)
calls (the ones around lines 29 and 65 that create tokens during auth flow) to
include an expiresIn option (for example "24h" or "7d") so tokens are
time-limited; ensure you pass the expiresIn value in the sign options object
(e.g., jwt.sign(payload, JWT_SECRET, { expiresIn: "24h" })) for both issuance
sites so the authenticate middleware will enforce expiry.

}

export function requireAdmin(req: AuthRequest, res: Response, next: NextFunction) {
// BUG: Checks role from the JWT payload which the user controls — should verify role from DB
if (req.user?.role !== "admin") {
return res.status(403).json({ error: "Admin access required" });
}
next();
Comment on lines +28 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not authorize admin access from token role claim alone.

requireAdmin currently trusts req.user.role from JWT payload. Role should be resolved server-side (DB/cache) to prevent stale or forged privilege claims.

As per coding guidelines, backend/src/middleware/**: Middleware: Check JWT validation logic, error handling, and ensure auth bypass is not possible. Verify token expiry handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/auth.ts` around lines 28 - 33, The requireAdmin
middleware must not trust req.user.role from the JWT; instead fetch the
authoritative user record server-side and verify its role before granting
access. In requireAdmin, use the user identifier available on AuthRequest (e.g.,
req.user?.id) to load the user via the canonical data access method (e.g.,
User.findById, userService.getById or cache lookup), handle missing user and DB
errors, check that the persisted user.role === "admin", respond 403 for
non-admins, and only call next() after successful verification; also preserve
existing JWT validation/expiry checks and ensure errors are logged/returned
appropriately.

}

export { JWT_SECRET };
74 changes: 74 additions & 0 deletions backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Router, Request, Response } from "express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import db from "../db";
import { JWT_SECRET } from "../middleware/auth";
import { isValidEmail } from "../utils/validate";

const router = Router();

// POST /api/auth/register
router.post("/register", async (req: Request, res: Response) => {
const { email, password, name } = req.body;

if (!email || !password || !name) {
return res.status(400).json({ error: "All fields are required" });
}

if (!isValidEmail(email)) {
return res.status(400).json({ error: "Invalid email" });
}

// BUG: No minimum password length or complexity check
const hashed = await bcrypt.hash(password, 10);
Comment on lines +22 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add password strength validation.

Passwords have no minimum length or complexity requirements. Weak passwords like "1" are accepted, making accounts vulnerable to brute-force attacks.

🔧 Proposed fix with password validation
+  if (password.length < 8) {
+    return res.status(400).json({ error: "Password must be at least 8 characters" });
+  }
+
   const hashed = await bcrypt.hash(password, 10);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/routes/auth.ts` around lines 22 - 23, Add server-side password
strength validation before calling bcrypt.hash(password, 10): ensure the
incoming password (the variable named password in the auth route handler) meets
a minimum length (e.g., >=8) and a simple complexity policy (e.g., contains
uppercase, lowercase, digit and/or special character) and return a 400 error
with a clear message if it fails; only call bcrypt.hash(password, 10) after
validation passes. Update the auth route handler (where bcrypt.hash is invoked)
to perform this check and short-circuit the request on invalid passwords.


try {
const stmt = db.prepare("INSERT INTO users (email, password, name) VALUES (?, ?, ?)");
const result = stmt.run(email, hashed, name);

const token = jwt.sign(
{ id: result.lastInsertRowid, email, role: "member" },
JWT_SECRET
// BUG: No token expiration — JWTs are valid forever
);
Comment on lines +29 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

JWTs never expire, creating a security risk.

Tokens without expiration remain valid forever. If a token is compromised, an attacker has permanent access. Additionally, users cannot be effectively logged out.

🔧 Proposed fix adding token expiration
     const token = jwt.sign(
       { id: result.lastInsertRowid, email, role: "member" },
-      JWT_SECRET
-      // BUG: No token expiration — JWTs are valid forever
+      JWT_SECRET,
+      { expiresIn: "24h" }
     );

Apply the same fix to the login route:

   const token = jwt.sign(
     { id: user.id, email: user.email, role: user.role },
-    JWT_SECRET
-    // BUG: No token expiration here either
+    JWT_SECRET,
+    { expiresIn: "24h" }
   );

Also applies to: 65-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/routes/auth.ts` around lines 29 - 33, The JWT creation currently
uses jwt.sign without an expiresIn option causing tokens to never expire; update
the jwt.sign calls (both in the signup flow where token is assigned to the token
constant and in the login flow around the jwt.sign at the 65-69 area) to include
an expiresIn value (e.g., '1h' or a configurable TTL) alongside JWT_SECRET so
generated tokens expire; ensure any tests/config refer to the chosen TTL or a
config variable for flexibility (refer to the token variable and jwt.sign
usage).


return res.status(201).json({ token, user: { id: result.lastInsertRowid, email, name, role: "member" } });
} catch (err: any) {
if (err.code === "SQLITE_CONSTRAINT_UNIQUE") {
return res.status(409).json({ error: "Email already registered" });
}
return res.status(500).json({ error: "Registration failed" });
}
});

// POST /api/auth/login
router.post("/login", async (req: Request, res: Response) => {
const { email, password } = req.body;

if (!email || !password) {
return res.status(400).json({ error: "Email and password required" });
}

// BUG: SQL injection — user input interpolated directly into query
const user = db.prepare(`SELECT * FROM users WHERE email = '${email}'`).get() as any;
Comment on lines +52 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CRITICAL: SQL injection vulnerability.

User input is directly interpolated into the SQL query. An attacker can bypass authentication or extract data by submitting a malicious email like: ' OR '1'='1' --

This allows complete database compromise.

🚨 Required fix using parameterized query
-  // BUG: SQL injection — user input interpolated directly into query
-  const user = db.prepare(`SELECT * FROM users WHERE email = '${email}'`).get() as any;
+  const user = db.prepare("SELECT * FROM users WHERE email = ?").get(email) as any;

As per coding guidelines: "API routes: Flag any raw SQL queries that don't use parameterized statements."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// BUG: SQL injection — user input interpolated directly into query
const user = db.prepare(`SELECT * FROM users WHERE email = '${email}'`).get() as any;
const user = db.prepare("SELECT * FROM users WHERE email = ?").get(email) as any;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/routes/auth.ts` around lines 52 - 53, The query is vulnerable to
SQL injection because email is interpolated into the SQL string; change the
db.prepare call to a parameterized statement (e.g. use a ? or named :email
placeholder) and pass the email as a parameter to .get() instead of embedding it
in the string — replace db.prepare(`SELECT * FROM users WHERE email =
'${email}'`).get() with something like db.prepare('SELECT * FROM users WHERE
email = ?').get(email) (or using a named param) so the variable is bound safely;
update the spot that assigns the user variable accordingly.


if (!user) {
// BUG: Leaks whether an email exists — allows user enumeration
return res.status(401).json({ error: "No account found with that email" });
}

const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(401).json({ error: "Incorrect password" });
}
Comment on lines +55 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

User enumeration vulnerability via distinct error messages.

Different error messages for "email not found" vs "incorrect password" allow attackers to enumerate valid email addresses. This information aids targeted attacks.

🔧 Proposed fix with generic error message
   if (!user) {
-    // BUG: Leaks whether an email exists — allows user enumeration
-    return res.status(401).json({ error: "No account found with that email" });
+    return res.status(401).json({ error: "Invalid email or password" });
   }

   const valid = await bcrypt.compare(password, user.password);
   if (!valid) {
-    return res.status(401).json({ error: "Incorrect password" });
+    return res.status(401).json({ error: "Invalid email or password" });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!user) {
// BUG: Leaks whether an email exists — allows user enumeration
return res.status(401).json({ error: "No account found with that email" });
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(401).json({ error: "Incorrect password" });
}
if (!user) {
return res.status(401).json({ error: "Invalid email or password" });
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(401).json({ error: "Invalid email or password" });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/routes/auth.ts` around lines 55 - 63, The login handler leaks
user existence by returning different messages for missing users vs wrong
passwords; change it to always return the same generic 401 error message (e.g.,
"Invalid email or password") and avoid timing differences by performing a
bcrypt.compare with a constant/dummy hash when user is null. Update the branch
that checks user (the user variable) to not short-circuit with a distinct
message and ensure the subsequent bcrypt.compare call (the valid variable) runs
against either user.password or a predefined dummy hash so the response and
timing are identical regardless of whether user exists.


const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET
// BUG: No token expiration here either
);

return res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role } });
});

export default router;
Loading