Skip to content

Add expense tracker app with CodeRabbit review config#7

Closed
reeder32 wants to merge 3 commits intomainfrom
demo/coderabbit-review
Closed

Add expense tracker app with CodeRabbit review config#7
reeder32 wants to merge 3 commits intomainfrom
demo/coderabbit-review

Conversation

@reeder32
Copy link
Copy Markdown
Collaborator

@reeder32 reeder32 commented Mar 4, 2026

Summary

  • Adds full-stack TypeScript expense tracker (Express + React + SQLite) with intentional bugs for review testing
  • Adds .coderabbit.yaml with assertive review profile, path-specific instructions, security/performance quality gates, and sequence diagrams
  • Removes Claude and OpenAI GitHub Actions workflows to isolate CodeRabbit as the sole review tool for this demo

What to look for

CodeRabbit should automatically review this PR and demonstrate:

  • Executive summary with risk assessment
  • Sequence diagrams for complex flows
  • Inline comments on security issues (SQL injection, auth bypass)
  • Performance warnings (N+1 queries, memory leaks)
  • Suggested reviewers and labels

Summary by CodeRabbit

New Features

  • User authentication with login functionality
  • Expense creation and management with budget tracking
  • Spending reports with category-based breakdowns
  • Team expense visibility and reporting for administrators
  • Dashboard with spending summary and budget overview
  • CSV export of expense records for administrators

Chores

  • Updated automated code review configuration and CI/CD workflows

reeder32 and others added 3 commits March 3, 2026 15:00
Adds full backend (Express/SQLite) and frontend (React) source files with
annotated bugs across all review categories: SQL injection, auth vulnerabilities,
logic errors, performance/memory leaks, and accessibility violations. Also aligns
OpenAI workflow prompt with Claude workflow for fair comparison.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Claude: instruct action to post inline review comments per finding
- OpenAI: request structured JSON, use pulls.createReview for inline comments
  with fallback to table format if line mapping fails
Configure assertive review profile with path-specific instructions,
security/performance quality gates, sequence diagrams, and auto-review
for the expense tracker app.
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add CodeRabbit review configuration and full-stack expense tracker demo app

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Adds full-stack TypeScript expense tracker with intentional bugs for review testing
• Introduces .coderabbit.yaml configuration with assertive review profile and quality gates
• Removes Claude and OpenAI GitHub Actions workflows to isolate CodeRabbit as sole reviewer
• Implements backend (Express/SQLite) and frontend (React) with annotated security, performance, and
  accessibility vulnerabilities
Diagram
flowchart LR
  A["CodeRabbit Config"] -->|"assertive profile"| B["Review Settings"]
  B -->|"path-specific instructions"| C["Backend Routes"]
  B -->|"path-specific instructions"| D["Frontend Components"]
  C -->|"SQL injection, auth bugs"| E["Intentional Vulnerabilities"]
  D -->|"XSS, a11y issues"| E
  F["Remove Workflows"] -->|"Claude & OpenAI"| G["Isolate CodeRabbit"]
Loading

Grey Divider

File Changes

1. .coderabbit.yaml ⚙️ Configuration changes +152/-0

CodeRabbit review configuration with assertive profile

.coderabbit.yaml


2. .github/workflows/claude-code-review.yml ⚙️ Configuration changes +0/-40

Remove Claude GitHub Actions workflow

.github/workflows/claude-code-review.yml


3. .github/workflows/openai-code-review.yml ⚙️ Configuration changes +0/-90

Remove OpenAI GitHub Actions workflow

.github/workflows/openai-code-review.yml


View more (16)
4. backend/src/db.ts ✨ Enhancement +30/-0

SQLite database initialization with schema

backend/src/db.ts


5. backend/src/index.ts 🐞 Bug fix +30/-0

Express server setup with intentional security bugs

backend/src/index.ts


6. backend/src/middleware/auth.ts 🐞 Bug fix +36/-0

JWT authentication with hardcoded secret and bypass vulnerabilities

backend/src/middleware/auth.ts


7. backend/src/routes/auth.ts 🐞 Bug fix +74/-0

Authentication endpoints with SQL injection and weak password validation

backend/src/routes/auth.ts


8. backend/src/routes/expenses.ts 🐞 Bug fix +107/-0

Expense CRUD endpoints with IDOR and race condition bugs

backend/src/routes/expenses.ts


9. backend/src/routes/reports.ts 🐞 Bug fix +58/-0

Report generation with memory leak and CSV injection vulnerabilities

backend/src/routes/reports.ts


10. backend/src/utils/validate.ts 🐞 Bug fix +15/-0

Input validation utilities with permissive regex patterns

backend/src/utils/validate.ts


11. frontend/src/App.tsx 🐞 Bug fix +29/-0

React router setup with weak token validation

frontend/src/App.tsx


12. frontend/src/api/client.ts 🐞 Bug fix +19/-0

Axios client with localStorage token storage and missing error handling

frontend/src/api/client.ts


13. frontend/src/components/ExpenseCard.tsx 🐞 Bug fix +49/-0

Expense card component with XSS and accessibility violations

frontend/src/components/ExpenseCard.tsx


14. frontend/src/components/StatusBadge.tsx 🐞 Bug fix +31/-0

Status badge with color-only indication and missing ARIA labels

frontend/src/components/StatusBadge.tsx


15. frontend/src/pages/Dashboard.tsx 🐞 Bug fix +61/-0

Dashboard page with memory leak and multiple accessibility issues

frontend/src/pages/Dashboard.tsx


16. frontend/src/pages/ExpenseForm.tsx 🐞 Bug fix +72/-0

Expense form with weak validation and missing form labels

frontend/src/pages/ExpenseForm.tsx


17. frontend/src/pages/ExpenseList.tsx 🐞 Bug fix +69/-0

Expense list with API hammering and missing list semantics

frontend/src/pages/ExpenseList.tsx


18. frontend/src/pages/Login.tsx 🐞 Bug fix +57/-0

Login page with error message leakage and accessibility gaps

frontend/src/pages/Login.tsx


19. frontend/src/pages/Reports.tsx 🐞 Bug fix +67/-0

Reports page with silent error handling and null reference bugs

frontend/src/pages/Reports.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Mar 4, 2026

Code Review by Qodo

🐞 Bugs (10) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. SQLi in login 🐞 Bug ⛨ Security
Description
The login endpoint interpolates user-controlled email directly into SQL, allowing SQL injection to
bypass authentication or exfiltrate user data.
Code

backend/src/routes/auth.ts[R52-58]

+  // BUG: SQL injection — user input interpolated directly into query
+  const user = db.prepare(`SELECT * FROM users WHERE email = '${email}'`).get() as any;
+
+  if (!user) {
+    // BUG: Leaks whether an email exists — allows user enumeration
+    return res.status(401).json({ error: "No account found with that email" });
+  }
Evidence
The query is constructed via string interpolation with untrusted input (email), which is executed
directly by SQLite.

backend/src/routes/auth.ts[45-58]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`POST /api/auth/login` uses string interpolation for `email` in SQL, enabling SQL injection and user enumeration.

## Issue Context
Use `better-sqlite3` parameter binding (`?`) and avoid revealing whether an email exists.

## Fix Focus Areas
- backend/src/routes/auth.ts[45-63]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Insecure JWT auth 🐞 Bug ⛨ Security
Description
JWT authentication is insecure: a hardcoded signing secret, non-expiring tokens, lax Authorization
header parsing, and admin authorization based only on token claims enable token forgery and
privilege escalation.
Code

backend/src/middleware/auth.ts[R4-33]

+const JWT_SECRET = "supersecretkey123"; // BUG: Hardcoded JWT secret — should use environment variable
+
+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 ", "");
+
+  try {
+    const payload = jwt.verify(token, JWT_SECRET) as any;
+    req.user = payload;
+    next();
+  } catch {
+    return res.status(401).json({ error: "Invalid token" });
+  }
+}
+
+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();
Evidence
The signing secret is hardcoded in source and used for verification; tokens are signed without
expiration; admin authorization trusts req.user.role directly from JWT payload; and the Bearer
prefix is not strictly validated.

backend/src/middleware/auth.ts[4-33]
backend/src/routes/auth.ts[29-34]
backend/src/routes/auth.ts[65-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
JWT auth is vulnerable to token forgery and privilege escalation due to (1) hardcoded `JWT_SECRET`, (2) missing token expiry, (3) weak Authorization parsing, and (4) `requireAdmin` trusting JWT role claim.

## Issue Context
Hardcoded secret + role-in-token check means anyone with the secret can mint an `admin` token. Even without leakage, role changes won&#x27;t be reflected, and long-lived tokens increase blast radius.

## Fix Focus Areas
- backend/src/middleware/auth.ts[4-34]
- backend/src/routes/auth.ts[25-35]
- backend/src/routes/auth.ts[65-71]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Expenses list SQLi 🐞 Bug ⛨ Security
Description
The expenses listing endpoint interpolates the category filter into SQL and allows unbounded
limit, enabling SQL injection and large data dumps/DoS.
Code

backend/src/routes/expenses.ts[R50-66]

+router.get("/", (req: AuthRequest, res: Response) => {
+  const userId = req.user!.id;
+  const page = parseInt(req.query.page as string) || 1;
+  const limit = parseInt(req.query.limit as string) || 20;
+  // BUG: No upper bound on limit — a client can request limit=999999 and dump the entire table
+  const offset = (page - 1) * limit;
+
+  // BUG: SQL injection — category filter is interpolated directly into the query string
+  const category = req.query.category as string;
+  let query = `SELECT * FROM expenses WHERE user_id = ?`;
+  if (category) {
+    query += ` AND category = '${category}'`;
+  }
+  query += ` ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`;
+
+  const expenses = db.prepare(query).all(userId);
+  return res.json({ expenses, page, limit });
Evidence
category is concatenated into the SQL string with quotes, and limit is directly embedded,
allowing attackers to inject SQL via category and request extremely large pages.

backend/src/routes/expenses.ts[50-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GET /api/expenses` builds SQL dynamically with user-controlled `category` and an unbounded `limit`, enabling SQL injection and data dumping/DoS.

## Issue Context
Prefer `WHERE user_id = ? AND category = ?` with parameters and enforce `1 &lt;= limit &lt;= MAX_LIMIT`.

## Fix Focus Areas
- backend/src/routes/expenses.ts[49-67]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (6)
4. Expense status authz 🐞 Bug ⛨ Security
Description
Any authenticated user can approve/reject any expense because the status endpoint lacks admin
authorization and ownership/self-approval checks.
Code

backend/src/routes/expenses.ts[R69-88]

+// PATCH /api/expenses/:id/status
+router.patch("/:id/status", (req: AuthRequest, res: Response) => {
+  const { status } = req.body;
+  const expenseId = req.params.id;
+
+  if (!["approved", "rejected"].includes(status)) {
+    return res.status(400).json({ error: "Status must be 'approved' or 'rejected'" });
+  }
+
+  // BUG: No admin check — any authenticated user can approve/reject any expense
+  const expense = db.prepare("SELECT * FROM expenses WHERE id = ?").get(expenseId) as any;
+
+  if (!expense) {
+    return res.status(404).json({ error: "Expense not found" });
+  }
+
+  // BUG: Users can approve their own expenses — no self-approval guard
+  db.prepare("UPDATE expenses SET status = ? WHERE id = ?").run(status, expenseId);
+  return res.json({ ...expense, status });
+});
Evidence
The endpoint is guarded only by authenticate at the router level; it selects by id and updates
status without verifying the caller is an admin or preventing self-approval.

backend/src/routes/expenses.ts[6-8]
backend/src/routes/expenses.ts[69-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PATCH /api/expenses/:id/status` allows any authenticated user to change any expense status.

## Issue Context
Use `requireAdmin` (after fixing it to verify role from DB) and add business-rule checks (e.g., no self-approval).

## Fix Focus Areas
- backend/src/routes/expenses.ts[69-88]
- backend/src/middleware/auth.ts[28-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Expense delete IDOR 🐞 Bug ⛨ Security
Description
DELETE /api/expenses/:id deletes by id without verifying the expense belongs to the requesting user,
enabling IDOR and unauthorized deletion.
Code

backend/src/routes/expenses.ts[R90-104]

+// DELETE /api/expenses/:id
+router.delete("/:id", (req: AuthRequest, res: Response) => {
+  const expenseId = req.params.id;
+  const userId = req.user!.id;
+
+  const expense = db.prepare("SELECT * FROM expenses WHERE id = ?").get(expenseId) as any;
+
+  if (!expense) {
+    return res.status(404).json({ error: "Expense not found" });
+  }
+
+  // BUG: IDOR — only checks if expense exists, not if it belongs to the requesting user
+  // Any authenticated user can delete any other user's expense
+  db.prepare("DELETE FROM expenses WHERE id = ?").run(expenseId);
+  return res.json({ message: "Expense deleted" });
Evidence
The route looks up an expense by id and deletes it without checking expense.user_id matches
req.user.id (the userId variable is unused).

backend/src/routes/expenses.ts[90-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DELETE /api/expenses/:id` is vulnerable to IDOR because it deletes expenses without verifying ownership.

## Issue Context
Use a delete statement that includes `user_id` or first check `expense.user_id === req.user.id`.

## Fix Focus Areas
- backend/src/routes/expenses.ts[90-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Stored XSS via description 🐞 Bug ⛨ Security
Description
Expense descriptions are rendered as raw HTML in the UI, enabling stored XSS; because auth tokens
are stored in localStorage, XSS can steal tokens and take over accounts.
Code

frontend/src/components/ExpenseCard.tsx[R28-33]

+      {/* BUG: Renders description as raw HTML via dangerouslySetInnerHTML — XSS vulnerability
+         If a user submits '<img src=x onerror=alert(1)>' as a description, it executes */}
+      <div
+        style={{ margin: "8px 0", color: "#555" }}
+        dangerouslySetInnerHTML={{ __html: expense.description }}
+      />
Evidence
The frontend renders expense.description via dangerouslySetInnerHTML; the backend accepts and
stores arbitrary description strings without sanitization; the client reads JWT from localStorage
for every request, making token theft via XSS straightforward.

frontend/src/components/ExpenseCard.tsx[28-33]
backend/src/routes/expenses.ts[20-26]
backend/src/routes/expenses.ts[41-46]
frontend/src/api/client.ts[7-14]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Stored XSS: user-controlled `description` is stored and later rendered via `dangerouslySetInnerHTML`, enabling arbitrary script execution and token theft.

## Issue Context
React escapes content by default when rendered as `{text}`; using `dangerouslySetInnerHTML` bypasses this. Since tokens are read from `localStorage`, any XSS can exfiltrate them.

## Fix Focus Areas
- frontend/src/components/ExpenseCard.tsx[28-33]
- backend/src/routes/expenses.ts[20-46]
- frontend/src/api/client.ts[7-15]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Unsafe CSV export 🐞 Bug ⛨ Security
Description
The CSV export loads all expenses into memory and outputs unescaped user fields, enabling DoS (OOM)
and CSV injection when opened in spreadsheet programs.
Code

backend/src/routes/reports.ts[R34-55]

+// GET /api/reports/export
+router.get("/export", requireAdmin, (req: AuthRequest, res: Response) => {
+  // BUG: No pagination or streaming — loads ALL expenses into memory at once
+  // Will cause OOM on large datasets
+  const expenses = db.prepare(`
+    SELECT e.*, u.name as user_name, u.email as user_email
+    FROM expenses e
+    JOIN users u ON e.user_id = u.id
+    ORDER BY e.created_at DESC
+  `).all();
+
+  // BUG: No Content-Disposition header — browser won't prompt download
+  res.setHeader("Content-Type", "text/csv");
+
+  // BUG: CSV injection — user-controlled fields (description, name) are not escaped.
+  // A description like '=CMD("calc")' will execute in Excel when opened.
+  let csv = "ID,User,Email,Amount,Description,Category,Status,Date\n";
+  for (const e of expenses as any[]) {
+    csv += `${e.id},${e.user_name},${e.user_email},${e.amount},${e.description},${e.category},${e.status},${e.created_at}\n`;
+  }
+
+  return res.send(csv);
Evidence
The export uses .all() without pagination/streaming and concatenates unescaped user-controlled
fields (e.g., description/name) directly into CSV lines.

backend/src/routes/reports.ts[34-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GET /api/reports/export` can OOM the server and is vulnerable to CSV injection because it loads all rows and concatenates unescaped fields.

## Issue Context
Use a streaming response (or bounded pagination) and CSV escaping/quoting; neutralize fields starting with `=`, `+`, `-`, `@`.

## Fix Focus Areas
- backend/src/routes/reports.ts[34-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Reports page null crash 🐞 Bug ✓ Correctness
Description
Team summary rendering calls toFixed() on row.total, but backend can return NULL totals for
users with no expenses, causing a runtime crash.
Code

frontend/src/pages/Reports.tsx[R53-60]

+              {teamSummary.map((row: any) => (
+                <tr key={row.email}>
+                  <td style={{ padding: 8 }}>{row.name}</td>
+                  <td style={{ padding: 8 }}>{row.count}</td>
+                  {/* BUG: row.total can be null (user with no expenses) — toFixed() will throw */}
+                  <td style={{ padding: 8 }}>${row.total.toFixed(2)}</td>
+                </tr>
+              ))}
Evidence
Backend uses LEFT JOIN with SUM(e.amount) which returns NULL when no matching rows exist;
frontend assumes row.total is a number and calls toFixed() unconditionally.

backend/src/routes/reports.ts[24-29]
frontend/src/pages/Reports.tsx[53-59]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Reports page crashes when `row.total` is null for users with no expenses.

## Issue Context
SQLite `SUM()` returns NULL when all inputs are NULL / no matching rows; with `LEFT JOIN`, this occurs for users without expenses.

## Fix Focus Areas
- backend/src/routes/reports.ts[24-29]
- frontend/src/pages/Reports.tsx[53-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Insecure server defaults 🐞 Bug ⛨ Security
Description
Server enables wildcard CORS, has no rate limiting, and returns error stack traces to clients,
increasing attack surface and leaking sensitive internals.
Code

backend/src/index.ts[R10-24]

+// BUG: Wildcard CORS — allows any origin to make authenticated requests
+app.use(cors());
+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
+
+// 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 });
+});
Evidence
cors() without an origin allowlist permits any origin to read responses (when credentials/tokens
are present), there is no rate limiting on login or other endpoints, and the global error handler
sends stack traces to clients.

backend/src/index.ts[10-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Backend server defaults increase security risk: wildcard CORS, no rate limiting, and stack trace leakage.

## Issue Context
These settings commonly enable brute force attacks and information disclosure and can make other vulnerabilities easier to exploit.

## Fix Focus Areas
- backend/src/index.ts[10-24]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

10. SQLite FKs not enforced 🐞 Bug ⛯ Reliability
Description
DB schema declares a foreign key, but PRAGMA foreign_keys = ON is never set, so SQLite will not
enforce the constraint and can create orphaned expenses.
Code

backend/src/db.ts[R7-27]

+db.pragma("journal_mode = WAL");
+
+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)
+  );
Evidence
The code sets journal mode but does not enable foreign key enforcement; the schema declares a
foreign key that SQLite will ignore unless foreign_keys is enabled on the connection.

backend/src/db.ts[7-8]
backend/src/db.ts[18-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
SQLite foreign keys are declared but not enforced unless `PRAGMA foreign_keys = ON` is enabled.

## Issue Context
Without enforcement, application bugs can create orphaned `expenses.user_id` references, causing inconsistent reports and cleanup problems.

## Fix Focus Areas
- backend/src/db.ts[4-8]
- backend/src/db.ts[18-27]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 4, 2026

Caution

Review failed

Pull request was closed or merged during review

Warning

Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories.

📝 Walkthrough

Walkthrough

This PR introduces a complete expense-tracking application with a backend Express.js API, SQLite database, JWT authentication middleware, and a React frontend with multiple pages. It also adds CodeRabbit automated review configuration and removes legacy Claude/OpenAI review workflows.

Changes

Cohort / File(s) Summary
Configuration & Automation
.coderabbit.yaml, .github/workflows/claude-code-review.yml, .github/workflows/openai-code-review.yml
Added CodeRabbit review configuration with workflow automation, quality gates, and tool integrations. Removed legacy Claude and OpenAI code review workflows.
Backend Database & Initialization
backend/src/db.ts, backend/src/index.ts
Established SQLite database (WAL mode) with users and expenses tables, foreign key constraints. Created Express app entry point with CORS, JSON middleware, route mounting, and global error handler.
Backend Authentication & Middleware
backend/src/middleware/auth.ts, backend/src/routes/auth.ts
Implemented JWT-based authentication with hardcoded secret. Added register and login routes with password hashing (bcrypt). Contains noted security issues: no password validation, missing JWT expiration, SQL injection risk, potential user enumeration.
Backend Expense Management
backend/src/routes/expenses.ts, backend/src/routes/reports.ts
Created CRUD endpoints for expenses with budget limit enforcement and status management. Added reporting endpoints for per-category/user summaries and CSV export. Code comments flag race conditions, off-by-one bugs, SQL injection, IDOR vulnerabilities, and CSV injection risks.
Backend Utilities
backend/src/utils/validate.ts
Added validation functions for email format, positive numbers, and predefined expense categories.
Frontend Core & Routing
frontend/src/App.tsx, frontend/src/api/client.ts
Implemented main App component with route guarding via PrivateRoute. Created Axios HTTP client with request interceptor for Bearer token injection. Includes commented security concerns about localStorage XSS vulnerability and missing 401 interception.
Frontend Components
frontend/src/components/ExpenseCard.tsx, frontend/src/components/StatusBadge.tsx
Built reusable components for displaying expenses and status badges. Code includes accessibility gaps (missing semantic HTML, ARIA labels) and XSS risk from dangerouslySetInnerHTML.
Frontend Pages
frontend/src/pages/Login.tsx, frontend/src/pages/Dashboard.tsx, frontend/src/pages/ExpenseForm.tsx, frontend/src/pages/ExpenseList.tsx, frontend/src/pages/Reports.tsx
Implemented five pages: login with localStorage token storage, dashboard with spending summary, expense creation form, paginated expense list with filtering, and dual summary reports. Pages include comments noting missing landmarks, labels, ARIA attributes, and potential runtime errors.

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend as Frontend App
    participant API as Backend API
    participant DB as SQLite DB

    User->>Frontend: 1. Enter credentials & submit
    Frontend->>API: 2. POST /auth/login
    API->>DB: 3. Query user by email
    DB-->>API: 4. Return user record
    API->>API: 5. Verify password (bcrypt)
    API-->>Frontend: 6. Return JWT token
    Frontend->>Frontend: 7. Store token in localStorage
    Frontend->>Frontend: 8. Navigate to /dashboard
    
    User->>Frontend: 9. View dashboard
    Frontend->>API: 10. GET /reports/my-summary (with JWT)
    API->>DB: 11. Query expenses by user_id
    DB-->>API: 12. Return expense summary
    API-->>Frontend: 13. Return aggregated summary
    Frontend->>Frontend: 14. Render dashboard with totals
Loading
sequenceDiagram
    actor User
    participant Frontend as Frontend App
    participant API as Backend API
    participant DB as SQLite DB

    User->>Frontend: 1. Fill expense form & submit
    Frontend->>API: 2. POST /api/expenses (with JWT)
    API->>API: 3. Validate amount & category
    API->>DB: 4. Query user's total spending
    DB-->>API: 5. Return current total
    API->>API: 6. Check against BUDGET_LIMIT
    API->>DB: 7. INSERT new expense record
    DB-->>API: 8. Return created expense
    API-->>Frontend: 9. Return expense with status
    Frontend->>Frontend: 10. Navigate to /expenses list
    
    User->>Frontend: 11. View expense list
    Frontend->>API: 12. GET /api/expenses (with pagination & JWT)
    API->>DB: 13. Query expenses with pagination
    DB-->>API: 14. Return paginated results
    API-->>Frontend: 15. Return expenses array
    Frontend->>Frontend: 16. Render expense cards
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A bouncing code review hops our way,
With databases and auth in play,
From login to expenses, dashboards bright,
A full-stack app takes its first flight!
Though security notes dot the code today,
This app is ready to expense away!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding a full-stack expense tracker app and CodeRabbit review configuration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch demo/coderabbit-review

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

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

Choose a reason for hiding this comment

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

Action required

1. Sqli in login 🐞 Bug ⛨ Security

The login endpoint interpolates user-controlled email directly into SQL, allowing SQL injection to
bypass authentication or exfiltrate user data.
Agent Prompt
## Issue description
`POST /api/auth/login` uses string interpolation for `email` in SQL, enabling SQL injection and user enumeration.

## Issue Context
Use `better-sqlite3` parameter binding (`?`) and avoid revealing whether an email exists.

## Fix Focus Areas
- backend/src/routes/auth.ts[45-63]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +4 to +33
const JWT_SECRET = "supersecretkey123"; // BUG: Hardcoded JWT secret — should use environment variable

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 ", "");

try {
const payload = jwt.verify(token, JWT_SECRET) as any;
req.user = payload;
next();
} catch {
return res.status(401).json({ error: "Invalid token" });
}
}

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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Insecure jwt auth 🐞 Bug ⛨ Security

JWT authentication is insecure: a hardcoded signing secret, non-expiring tokens, lax Authorization
header parsing, and admin authorization based only on token claims enable token forgery and
privilege escalation.
Agent Prompt
## Issue description
JWT auth is vulnerable to token forgery and privilege escalation due to (1) hardcoded `JWT_SECRET`, (2) missing token expiry, (3) weak Authorization parsing, and (4) `requireAdmin` trusting JWT role claim.

## Issue Context
Hardcoded secret + role-in-token check means anyone with the secret can mint an `admin` token. Even without leakage, role changes won't be reflected, and long-lived tokens increase blast radius.

## Fix Focus Areas
- backend/src/middleware/auth.ts[4-34]
- backend/src/routes/auth.ts[25-35]
- backend/src/routes/auth.ts[65-71]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +50 to +66
router.get("/", (req: AuthRequest, res: Response) => {
const userId = req.user!.id;
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 20;
// BUG: No upper bound on limit — a client can request limit=999999 and dump the entire table
const offset = (page - 1) * limit;

// BUG: SQL injection — category filter is interpolated directly into the query string
const category = req.query.category as string;
let query = `SELECT * FROM expenses WHERE user_id = ?`;
if (category) {
query += ` AND category = '${category}'`;
}
query += ` ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`;

const expenses = db.prepare(query).all(userId);
return res.json({ expenses, page, limit });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Expenses list sqli 🐞 Bug ⛨ Security

The expenses listing endpoint interpolates the category filter into SQL and allows unbounded
limit, enabling SQL injection and large data dumps/DoS.
Agent Prompt
## Issue description
`GET /api/expenses` builds SQL dynamically with user-controlled `category` and an unbounded `limit`, enabling SQL injection and data dumping/DoS.

## Issue Context
Prefer `WHERE user_id = ? AND category = ?` with parameters and enforce `1 <= limit <= MAX_LIMIT`.

## Fix Focus Areas
- backend/src/routes/expenses.ts[49-67]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +69 to +88
// PATCH /api/expenses/:id/status
router.patch("/:id/status", (req: AuthRequest, res: Response) => {
const { status } = req.body;
const expenseId = req.params.id;

if (!["approved", "rejected"].includes(status)) {
return res.status(400).json({ error: "Status must be 'approved' or 'rejected'" });
}

// BUG: No admin check — any authenticated user can approve/reject any expense
const expense = db.prepare("SELECT * FROM expenses WHERE id = ?").get(expenseId) as any;

if (!expense) {
return res.status(404).json({ error: "Expense not found" });
}

// BUG: Users can approve their own expenses — no self-approval guard
db.prepare("UPDATE expenses SET status = ? WHERE id = ?").run(status, expenseId);
return res.json({ ...expense, status });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Expense status authz 🐞 Bug ⛨ Security

Any authenticated user can approve/reject any expense because the status endpoint lacks admin
authorization and ownership/self-approval checks.
Agent Prompt
## Issue description
`PATCH /api/expenses/:id/status` allows any authenticated user to change any expense status.

## Issue Context
Use `requireAdmin` (after fixing it to verify role from DB) and add business-rule checks (e.g., no self-approval).

## Fix Focus Areas
- backend/src/routes/expenses.ts[69-88]
- backend/src/middleware/auth.ts[28-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +90 to +104
// DELETE /api/expenses/:id
router.delete("/:id", (req: AuthRequest, res: Response) => {
const expenseId = req.params.id;
const userId = req.user!.id;

const expense = db.prepare("SELECT * FROM expenses WHERE id = ?").get(expenseId) as any;

if (!expense) {
return res.status(404).json({ error: "Expense not found" });
}

// BUG: IDOR — only checks if expense exists, not if it belongs to the requesting user
// Any authenticated user can delete any other user's expense
db.prepare("DELETE FROM expenses WHERE id = ?").run(expenseId);
return res.json({ message: "Expense deleted" });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Expense delete idor 🐞 Bug ⛨ Security

DELETE /api/expenses/:id deletes by id without verifying the expense belongs to the requesting user,
enabling IDOR and unauthorized deletion.
Agent Prompt
## Issue description
`DELETE /api/expenses/:id` is vulnerable to IDOR because it deletes expenses without verifying ownership.

## Issue Context
Use a delete statement that includes `user_id` or first check `expense.user_id === req.user.id`.

## Fix Focus Areas
- backend/src/routes/expenses.ts[90-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +28 to +33
{/* BUG: Renders description as raw HTML via dangerouslySetInnerHTML — XSS vulnerability
If a user submits '<img src=x onerror=alert(1)>' as a description, it executes */}
<div
style={{ margin: "8px 0", color: "#555" }}
dangerouslySetInnerHTML={{ __html: expense.description }}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

6. Stored xss via description 🐞 Bug ⛨ Security

Expense descriptions are rendered as raw HTML in the UI, enabling stored XSS; because auth tokens
are stored in localStorage, XSS can steal tokens and take over accounts.
Agent Prompt
## Issue description
Stored XSS: user-controlled `description` is stored and later rendered via `dangerouslySetInnerHTML`, enabling arbitrary script execution and token theft.

## Issue Context
React escapes content by default when rendered as `{text}`; using `dangerouslySetInnerHTML` bypasses this. Since tokens are read from `localStorage`, any XSS can exfiltrate them.

## Fix Focus Areas
- frontend/src/components/ExpenseCard.tsx[28-33]
- backend/src/routes/expenses.ts[20-46]
- frontend/src/api/client.ts[7-15]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +34 to +55
// GET /api/reports/export
router.get("/export", requireAdmin, (req: AuthRequest, res: Response) => {
// BUG: No pagination or streaming — loads ALL expenses into memory at once
// Will cause OOM on large datasets
const expenses = db.prepare(`
SELECT e.*, u.name as user_name, u.email as user_email
FROM expenses e
JOIN users u ON e.user_id = u.id
ORDER BY e.created_at DESC
`).all();

// BUG: No Content-Disposition header — browser won't prompt download
res.setHeader("Content-Type", "text/csv");

// BUG: CSV injection — user-controlled fields (description, name) are not escaped.
// A description like '=CMD("calc")' will execute in Excel when opened.
let csv = "ID,User,Email,Amount,Description,Category,Status,Date\n";
for (const e of expenses as any[]) {
csv += `${e.id},${e.user_name},${e.user_email},${e.amount},${e.description},${e.category},${e.status},${e.created_at}\n`;
}

return res.send(csv);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

7. Unsafe csv export 🐞 Bug ⛨ Security

The CSV export loads all expenses into memory and outputs unescaped user fields, enabling DoS (OOM)
and CSV injection when opened in spreadsheet programs.
Agent Prompt
## Issue description
`GET /api/reports/export` can OOM the server and is vulnerable to CSV injection because it loads all rows and concatenates unescaped fields.

## Issue Context
Use a streaming response (or bounded pagination) and CSV escaping/quoting; neutralize fields starting with `=`, `+`, `-`, `@`.

## Fix Focus Areas
- backend/src/routes/reports.ts[34-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +53 to +60
{teamSummary.map((row: any) => (
<tr key={row.email}>
<td style={{ padding: 8 }}>{row.name}</td>
<td style={{ padding: 8 }}>{row.count}</td>
{/* BUG: row.total can be null (user with no expenses) — toFixed() will throw */}
<td style={{ padding: 8 }}>${row.total.toFixed(2)}</td>
</tr>
))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

8. Reports page null crash 🐞 Bug ✓ Correctness

Team summary rendering calls toFixed() on row.total, but backend can return NULL totals for
users with no expenses, causing a runtime crash.
Agent Prompt
## Issue description
Reports page crashes when `row.total` is null for users with no expenses.

## Issue Context
SQLite `SUM()` returns NULL when all inputs are NULL / no matching rows; with `LEFT JOIN`, this occurs for users without expenses.

## Fix Focus Areas
- backend/src/routes/reports.ts[24-29]
- frontend/src/pages/Reports.tsx[53-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread backend/src/index.ts
Comment on lines +10 to +24
// BUG: Wildcard CORS — allows any origin to make authenticated requests
app.use(cors());
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

// 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 });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

9. Insecure server defaults 🐞 Bug ⛨ Security

Server enables wildcard CORS, has no rate limiting, and returns error stack traces to clients,
increasing attack surface and leaking sensitive internals.
Agent Prompt
## Issue description
Backend server defaults increase security risk: wildcard CORS, no rate limiting, and stack trace leakage.

## Issue Context
These settings commonly enable brute force attacks and information disclosure and can make other vulnerabilities easier to exploit.

## Fix Focus Areas
- backend/src/index.ts[10-24]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@reeder32 reeder32 closed this Mar 4, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 4, 2026

Greptile Summary

This PR introduces a full-stack TypeScript expense tracker (Express + React + SQLite) alongside a .coderabbit.yaml configuration, removing two GitHub Actions AI-review workflows. The application contains multiple critical security vulnerabilities that require immediate attention before any production deployment:

Critical issues:

  • SQL injection (2 locations): backend/src/routes/auth.ts line 53 interpolates email into login query; backend/src/routes/expenses.ts lines 59–63 interpolate category and limit directly into SQL strings. Both allow attackers to read, modify, or destroy the database.
  • Stored XSS: frontend/src/components/ExpenseCard.tsx renders unsanitized user descriptions via dangerouslySetInnerHTML, allowing script injection to execute in other users' browsers.
  • Hardcoded JWT signing key: backend/src/middleware/auth.ts line 4 commits the key to source control. Anyone with repo access can forge tokens for any user or role.
  • IDOR on delete: backend/src/routes/expenses.ts DELETE handler never verifies expense ownership, allowing any authenticated user to delete any other user's data.
  • Privilege escalation: backend/src/routes/expenses.ts PATCH status route lacks requireAdmin middleware, allowing any employee to approve/reject any expense.
  • CSV formula injection: backend/src/routes/reports.ts export endpoint writes unsanitized user data into CSV cells, enabling formula execution in spreadsheet applications.
  • JWTs with no expiration: Both /register and /login routes issue tokens without expiresIn, so leaked tokens remain valid forever.

High-severity issues:

  • Admin role authorization checks JWT payload instead of database, enabling token forgery.
  • Stack traces exposed in error responses leak internal paths and library versions to clients.

Medium-severity issues:

  • frontend/src/pages/Reports.tsx line 58 crashes when calling .toFixed() on null for users with no expenses.
  • Missing useEffect cleanup in Dashboard, ExpenseList, and Reports components risks setState on unmounted components.

Confidence Score: 0/5

  • This PR contains multiple critical vulnerabilities (SQL injection, XSS, IDOR, hardcoded secrets) that would enable complete compromise of user data and authentication. Not safe to merge.
  • 12 verified issues across 8 critical/high-severity categories. Four files have 0/5 confidence scores due to authentication bypass, data breach, and code execution risks. The hardcoded JWT secret combined with SQL injection and XSS creates a complete security breakdown. This is a demonstration codebase intentionally seeded with bugs for review-tooling testing.
  • backend/src/routes/auth.ts (SQL injection, JWT expiration), backend/src/routes/expenses.ts (SQL injection, IDOR, privilege escalation), backend/src/middleware/auth.ts (hardcoded secret, role bypass), frontend/src/components/ExpenseCard.tsx (stored XSS)

Last reviewed commit: 33e03d5

}

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

Choose a reason for hiding this comment

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

Critical: SQL injection in login query

The email value from req.body is directly interpolated into the SQL string using a template literal. An attacker can log in as any user — or destroy data — by submitting an email like ' OR '1'='1' -- or '; DROP TABLE users; --.

Use parameterized queries instead:

Suggested change
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;

Comment on lines +28 to +33
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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High: Admin role authorization trusts JWT payload instead of database

requireAdmin checks req.user?.role which is read directly from the JWT claims. Because the JWT secret is hardcoded, an attacker who discovers it can craft a token with { "role": "admin" } and gain full admin access. Even with a strong secret, roles should be verified against the database on each request so that a revoked privilege takes effect immediately.

Verify the current database role rather than relying solely on the token payload.

Comment on lines +59 to +63
let query = `SELECT * FROM expenses WHERE user_id = ?`;
if (category) {
query += ` AND category = '${category}'`;
}
query += ` ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: SQL injection in category filter and unbounded LIMIT

The category query parameter is interpolated directly into the SQL string on line 61. An attacker can inject arbitrary SQL via the ?category= parameter (e.g., ' OR '1'='1) to access other users' expenses or exfiltrate the entire table.

Additionally, the limit value from the query string is interpolated into the LIMIT clause on line 63 with no upper-bound cap, allowing a client to pass limit=999999 and dump the entire expenses table in one request.

Use parameterized queries and cap the limit:

Suggested change
let query = `SELECT * FROM expenses WHERE user_id = ?`;
if (category) {
query += ` AND category = '${category}'`;
}
query += ` ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`;
let query = `SELECT * FROM expenses WHERE user_id = ?`;
const params: any[] = [userId];
if (category) {
query += ` AND category = ?`;
params.push(category);
}
query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`;
const safeLimit = Math.min(limit, 100);
const safeOffset = (page - 1) * safeLimit;
params.push(safeLimit, safeOffset);
const expenses = db.prepare(query).all(...params);

Comment on lines +70 to +88
router.patch("/:id/status", (req: AuthRequest, res: Response) => {
const { status } = req.body;
const expenseId = req.params.id;

if (!["approved", "rejected"].includes(status)) {
return res.status(400).json({ error: "Status must be 'approved' or 'rejected'" });
}

// BUG: No admin check — any authenticated user can approve/reject any expense
const expense = db.prepare("SELECT * FROM expenses WHERE id = ?").get(expenseId) as any;

if (!expense) {
return res.status(404).json({ error: "Expense not found" });
}

// BUG: Users can approve their own expenses — no self-approval guard
db.prepare("UPDATE expenses SET status = ? WHERE id = ?").run(status, expenseId);
return res.json({ ...expense, status });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High: Missing admin authorization on expense status update (privilege escalation)

The PATCH /:id/status route applies authenticate (any logged-in user) but does not call requireAdmin. Any regular employee can approve or reject any expense, including their own — bypassing the intended approval workflow entirely.

The requireAdmin middleware already exists. Add it to this route:

Suggested change
router.patch("/:id/status", (req: AuthRequest, res: Response) => {
const { status } = req.body;
const expenseId = req.params.id;
if (!["approved", "rejected"].includes(status)) {
return res.status(400).json({ error: "Status must be 'approved' or 'rejected'" });
}
// BUG: No admin check — any authenticated user can approve/reject any expense
const expense = db.prepare("SELECT * FROM expenses WHERE id = ?").get(expenseId) as any;
if (!expense) {
return res.status(404).json({ error: "Expense not found" });
}
// BUG: Users can approve their own expenses — no self-approval guard
db.prepare("UPDATE expenses SET status = ? WHERE id = ?").run(status, expenseId);
return res.json({ ...expense, status });
});
router.patch("/:id/status", requireAdmin, (req: AuthRequest, res: Response) => {

Comment on lines +91 to +105
router.delete("/:id", (req: AuthRequest, res: Response) => {
const expenseId = req.params.id;
const userId = req.user!.id;

const expense = db.prepare("SELECT * FROM expenses WHERE id = ?").get(expenseId) as any;

if (!expense) {
return res.status(404).json({ error: "Expense not found" });
}

// BUG: IDOR — only checks if expense exists, not if it belongs to the requesting user
// Any authenticated user can delete any other user's expense
db.prepare("DELETE FROM expenses WHERE id = ?").run(expenseId);
return res.json({ message: "Expense deleted" });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: IDOR — any authenticated user can delete any expense

The delete handler retrieves the expense by ID but never checks that expense.user_id === userId. User A can permanently delete User B's expense by knowing (or guessing) its numeric ID.

Add ownership verification before deletion:

Suggested change
router.delete("/:id", (req: AuthRequest, res: Response) => {
const expenseId = req.params.id;
const userId = req.user!.id;
const expense = db.prepare("SELECT * FROM expenses WHERE id = ?").get(expenseId) as any;
if (!expense) {
return res.status(404).json({ error: "Expense not found" });
}
// BUG: IDOR — only checks if expense exists, not if it belongs to the requesting user
// Any authenticated user can delete any other user's expense
db.prepare("DELETE FROM expenses WHERE id = ?").run(expenseId);
return res.json({ message: "Expense deleted" });
});
if (!expense) {
return res.status(404).json({ error: "Expense not found" });
}
if (expense.user_id !== userId) {
return res.status(403).json({ error: "Forbidden" });
}
db.prepare("DELETE FROM expenses WHERE id = ?").run(expenseId);
return res.json({ message: "Expense deleted" });

Comment on lines +8 to +14
useEffect(() => {
// BUG: No cleanup/abort — if the component unmounts before the request completes,
// this will attempt to setState on an unmounted component (memory leak)
client.get("/reports/my-summary").then((res) => {
setSummary(res.data.summary);
});
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: Missing useEffect cleanup — potential state update on unmounted component

If the user navigates away before the fetch resolves, setSummary will be called on an unmounted component, producing a React warning and a potential memory leak. The same pattern exists in ExpenseList.tsx (lines 19–24) and Reports.tsx (lines 8–13).

Use an AbortController to cancel in-flight requests:

Suggested change
useEffect(() => {
// BUG: No cleanup/abort — if the component unmounts before the request completes,
// this will attempt to setState on an unmounted component (memory leak)
client.get("/reports/my-summary").then((res) => {
setSummary(res.data.summary);
});
}, []);
useEffect(() => {
const controller = new AbortController();
client.get("/reports/my-summary", { signal: controller.signal }).then((res) => {
setSummary(res.data.summary);
}).catch((err) => {
if (err.name !== "AbortError") console.error(err);
});
return () => controller.abort();
}, []);

Comment on lines +48 to +53
// BUG: CSV injection — user-controlled fields (description, name) are not escaped.
// A description like '=CMD("calc")' will execute in Excel when opened.
let csv = "ID,User,Email,Amount,Description,Category,Status,Date\n";
for (const e of expenses as any[]) {
csv += `${e.id},${e.user_name},${e.user_email},${e.amount},${e.description},${e.category},${e.status},${e.created_at}\n`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High: CSV formula injection

User-controlled fields (description, user_name, user_email) are written directly into CSV cells without sanitization. A description starting with =, +, -, or @ is treated as a spreadsheet formula when the file is opened in Excel or Google Sheets, which can execute arbitrary commands on the recipient's machine.

Sanitize all user-controlled fields by:

  1. Wrapping each field in double quotes with inner double-quotes escaped (replace """)
  2. Prefixing with a single quote any cell value that begins with =, +, -, @, tab, or carriage return

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.

Critical: Hardcoded JWT signing key in source control

The JWT signing key is a short, static string committed directly to the repository. Anyone with read access can forge valid JWTs for any user or role. This is further worsened by the key being re-exported on line 36.

Replace it with an environment variable and fail at startup if missing. The key should never be stored in code.

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

Choose a reason for hiding this comment

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

Critical: JWTs issued without expiration

Neither the /register nor the /login jwt.sign calls include an expiresIn option. Tokens are valid indefinitely — a stolen or leaked token can never be invalidated without rotating the signing key.

Add a short-lived expiry (e.g., 8 hours) as the third argument to each jwt.sign() call. Include an options object with { expiresIn: "8h" }.

Comment on lines +65 to +69
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET
// BUG: No token expiration here either
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: JWTs issued without expiration (login route)

The /login route also fails to set expiresIn. Add token expiration here as well to prevent indefinite token validity. Include an options object with a short expiry time (e.g., 8 hours) as the third argument to jwt.sign().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant