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
11 changes: 9 additions & 2 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
contents: read
pull-requests: write
issues: write
id-token: write # ← was missing, required by the action
id-token: write
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

🧩 Analysis chain

🌐 Web query:

anthropics claude-code-action permission requirements id-token

💡 Result:

id-token: write is not inherently required by anthropics/claude-code-action anymore (it was removed in v1.0.6 along with adding a github_token input). [3]

You do need permissions: id-token: write in your workflow when you’re using GitHub Actions OIDC to authenticate to a cloud provider (e.g., AWS Bedrock via assuming an IAM role, or Google Vertex AI via Workload Identity Federation). Anthropic’s Claude Code GitHub Actions setup docs list OIDC-based auth as the recommended approach for Bedrock/Vertex. [1]

If you see errors like “Could not fetch an OIDC token… add id-token: write”, that’s exactly the missing-permissions failure mode reported by users when OIDC is in play. [2]

Typical pattern

  • Anthropic API key only: no id-token: write needed (just set the GitHub token permissions you need for repo operations).
  • Bedrock/Vertex (OIDC): add permissions: id-token: write (in addition to repo permissions like contents, pull-requests, issues, etc.). [1]

Sources: Claude Code GitHub Actions docs [1], GitHub issue report about missing id-token: write [2], claude-code-action release notes (v1.0.6) [3].


🌐 Web query:

anthropic claude-code-action action.yml github actions

💡 Result:

anthropics/claude-code-action defines its GitHub Action metadata in action.yml at the repo root. It’s a composite action (runs steps on the runner), installs/uses Bun, installs dependencies, then runs bun run .../src/entrypoints/run.ts. [1][2]

Key parts of action.yml you’ll likely care about: [2]

  • Name/description/branding
    • name: "Claude Code Action v1.0"
  • Inputs (selected)
    • trigger_phrase (default @claude)
    • label_trigger (default claude)
    • prompt (instructions for Claude; default empty)
    • claude_args (extra CLI args; default empty)
    • Auth/provider: anthropic_api_key, claude_code_oauth_token, plus toggles use_bedrock, use_vertex, use_foundry
    • github_token (optional; used if not using the GitHub App token flow)
  • Outputs (selected)
    • execution_file, branch_name, github_token, structured_output, session_id
  • Runs
    • runs: using: "composite" with steps including Bun setup + “Run Claude Code Action”

Anthropic’s docs show basic workflow usage like:
uses: anthropics/claude-code-action@v1 with anthropic_api_key, prompt, and optional claude_args. [3]

Sources
[1] anthropics/claude-code-action (repo) — https://github.com/anthropics/claude-code-action
[2] action.ymlhttps://github.com/anthropics/claude-code-action/blob/main/action.yml
[3] Anthropic docs: Claude Code GitHub Actions — https://docs.anthropic.com/en/docs/claude-code/github-actions


🌐 Web query:

github actions id-token write OIDC when required

💡 Result:

permissions: id-token: write is required only when a workflow/job needs to request a GitHub OIDC ID token (JWT)—i.e., when a step/action calls the OIDC endpoint (via ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN) or the Actions toolkit getIDToken()—typically to federate into a cloud/provider (AWS/GCP/Azure, etc.). Without it, the OIDC token cannot be requested. [1]

Add it at the workflow or job level, for example:

permissions:
  id-token: write
  contents: read

(Include any other permissions you need; setting permissions: can remove defaults.) [1][3]

Reusable workflows: if the called reusable workflow is outside your org/enterprise, you must explicitly grant id-token: write in the caller workflow/job that invokes it. [1][2]

Not required if you’re not using OIDC (e.g., only using GITHUB_TOKEN API access, repo checkout, artifacts, etc.). [1]


🏁 Script executed:

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

Repository: reeder32/teamexpense

Length of output: 2340


Remove the unnecessary id-token: write permission.

The workflow uses only anthropic_api_key for direct Anthropic API authentication and does not configure any cloud provider OIDC authentication (Bedrock, Vertex, or Foundry). The id-token: write permission is only required for OIDC-based federation to cloud providers. Since this workflow is not using that pattern, the permission grants an unnecessary privilege and violates the principle of least privilege.

Remove line 16 (id-token: write) from the permissions block.

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

In @.github/workflows/claude-code-review.yml at line 16, Remove the unnecessary
GitHub Actions permission entry "id-token: write" from the workflow permissions
block (the line shown as id-token: write) so the workflow only grants the
least-privilege permissions it needs for using the anthropic_api_key; locate the
permissions block in the claude-code-review.yml workflow and delete the
id-token: write line.

steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -24,11 +24,17 @@ jobs:
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }} # ← was missing
github_token: ${{ secrets.GITHUB_TOKEN }}
direct_prompt: |
Please review this entire PR for bugs. Focus on SQL injection and auth vulnerabilities,
logic errors, performance issues and memory leaks, and UI/UX accessibility violations.
Flag every issue with file, line number, severity, and explanation.

IMPORTANT: For each issue you find, post an inline review comment on the specific line
of the file where the issue occurs. Do NOT put all findings in a single comment — use
individual inline comments so each issue appears directly on the relevant line in the diff.
After posting all inline comments, post a brief summary comment listing the total count
of issues found by severity.
trigger_phrase: "@claude"
custom_instructions: |
You are reviewing a TypeScript expense tracker app (Node/Express + React + SQLite).
Expand All @@ -38,3 +44,4 @@ jobs:
3. Performance issues and memory leaks
4. UI/UX and accessibility violations
Be thorough — flag every issue you find with file, line, severity, and explanation.
Always post findings as inline review comments on the exact lines where the issues are.
116 changes: 92 additions & 24 deletions .github/workflows/openai-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@ jobs:
git diff origin/${{ github.base_ref }}...HEAD > pr_diff.txt

- name: OpenAI Review via API
id: review
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
DIFF=$(cat pr_diff.txt | head -c 20000)

# Write request body to a temp file to avoid shell escaping issues
python3 - <<'PYEOF'
import os, json, urllib.request, sys

Expand All @@ -37,17 +33,34 @@ jobs:

payload = json.dumps({
"model": "gpt-4o",
"response_format": {"type": "json_object"},
"messages": [
{
"role": "system",
"content": (
"You are a senior code reviewer. Review this PR diff for a TypeScript expense tracker "
"(Node/Express + React + SQLite). Find ALL bugs in these categories: SQL injection/auth "
"vulnerabilities, logic errors, performance/memory leaks, and accessibility issues. "
"For each bug: state the file, approximate line, severity (critical/high/medium/low), and explanation."
"You are reviewing a TypeScript expense tracker app (Node/Express + React + SQLite).\n"
"Focus on:\n"
"1. SQL injection and authentication vulnerabilities\n"
"2. Logic errors and off-by-one bugs\n"
"3. Performance issues and memory leaks\n"
"4. UI/UX and accessibility violations\n"
"Be thorough — flag every issue you find with file, line, severity, and explanation.\n\n"
"You MUST respond with a JSON object in this exact format:\n"
'{"comments": [{"path": "relative/file/path.ts", "line": 42, "severity": "critical|high|medium|low", "body": "explanation of the issue"}]}\n'
"The path must match the file path in the diff (e.g. backend/src/routes/auth.ts).\n"
"The line number must be a line that exists in the NEW version of the file in the diff.\n"
"Return ONLY the JSON object, no other text."
)
},
{"role": "user", "content": f"PR Diff:\n\n{diff}"}
{
"role": "user",
"content": (
"Please review this entire PR for bugs. Focus on SQL injection and auth vulnerabilities, "
"logic errors, performance issues and memory leaks, and UI/UX accessibility violations. "
"Flag every issue with file, line number, severity, and explanation.\n\n"
f"PR Diff:\n\n{diff}"
)
Comment on lines +58 to +62
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

Raw diff exfiltration risk to third-party API.

Line 61 sends the PR diff directly to an external provider. For private repos, this can leak secrets/PII from code changes unless redaction/allowlist controls are in place.

Recommended mitigation
-                  f"PR Diff:\n\n{diff}"
+                  "PR Diff (sensitive tokens redacted):\n\n"
+                  f"{diff.replace('OPENAI_API_KEY', 'REDACTED').replace('Authorization:', 'Authorization: REDACTED')}"
📝 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
"Please review this entire PR for bugs. Focus on SQL injection and auth vulnerabilities, "
"logic errors, performance issues and memory leaks, and UI/UX accessibility violations. "
"Flag every issue with file, line number, severity, and explanation.\n\n"
f"PR Diff:\n\n{diff}"
)
"Please review this entire PR for bugs. Focus on SQL injection and auth vulnerabilities, "
"logic errors, performance issues and memory leaks, and UI/UX accessibility violations. "
"Flag every issue with file, line number, severity, and explanation.\n\n"
"PR Diff (sensitive tokens redacted):\n\n"
f"{diff.replace('OPENAI_API_KEY', 'REDACTED').replace('Authorization:', 'Authorization: REDACTED')}"
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/openai-code-review.yml around lines 58 - 62, The workflow
currently injects the raw PR diff variable named diff into the external API
payload by concatenating "PR Diff:\n\n{diff}", which risks leaking secrets/PII;
update the payload construction to stop sending the raw diff and instead call a
sanitizer/redactor or produce an allowlisted summary (e.g., implement a
redactDiff(diff) or generateDiffSummary(diff) step) and replace "{diff}" with
the sanitized output, or only send metadata (changed files list, filenames, and
file types) and a hash of the diff; ensure the string building code that
contains "PR Diff:\n\n{diff}" is changed to use the sanitized variable and add
tests/logging to confirm no raw diff is transmitted.

}
]
}).encode()

Expand All @@ -63,28 +76,83 @@ jobs:
try:
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
review = data["choices"][0]["message"]["content"]
review_json = data["choices"][0]["message"]["content"]
except urllib.error.HTTPError as e:
body = e.read().decode()
print(f"OpenAI API error {e.code}: {body}", file=sys.stderr)
review = f"⚠️ OpenAI API error {e.code} — check that OPENAI_API_KEY secret is set correctly.\n\n{body}"
with open("review_result.json", "w") as f:
json.dump({"error": f"OpenAI API error {e.code}: {body}"}, f)
sys.exit(1)
Comment on lines +83 to +85
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

Error path currently blocks the reporting step.

Line 85 exits non-zero, so the next step won’t run under default success() conditions. That prevents posting the intended error comment from review_result.json.

Proposed fix
       - name: Post Inline Review Comments
+        if: always()
         uses: actions/github-script@v7

Also applies to: 91-91

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

In @.github/workflows/openai-code-review.yml around lines 83 - 85, The workflow
currently calls sys.exit(1) after writing review_result.json (the error path
that handles OpenAI API errors), which prevents subsequent steps from running
and stops the error comment from being posted; change the error handling in that
block (and the similar block around the other occurrence) so it writes
review_result.json and exits with a zero code or simply returns instead of
sys.exit(1) (i.e., remove or replace sys.exit(1) with a non-failing exit such as
sys.exit(0) or no exit), ensuring the step completes successfully and allows the
following reporting step to run and post the error comment.


with open(os.environ["GITHUB_ENV"], "a") as f:
f.write("OPENAI_REVIEW<<DELIM\n")
f.write(review + "\n")
f.write("DELIM\n")
with open("review_result.json", "w") as f:
f.write(review_json)
PYEOF

- name: Post Review Comment
- name: Post Inline Review Comments
uses: actions/github-script@v7
with:
script: |
const review = `## 🤖 OpenAI Code Review\n\n${process.env.OPENAI_REVIEW}`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: review
const fs = require('fs');
const result = JSON.parse(fs.readFileSync('review_result.json', 'utf8'));

if (result.error) {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## OpenAI Code Review\n\n⚠️ ${result.error}`
});
return;
}

const comments = result.comments || [];
if (comments.length === 0) {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## OpenAI Code Review\n\nNo issues found.'
});
return;
}

// Build inline comments for the review
const reviewComments = comments.map(c => ({
path: c.path,
line: c.line,
body: `**[${c.severity.toUpperCase()}]** ${c.body}`
}));

// Count by severity for summary
const counts = {};
comments.forEach(c => {
const s = c.severity.toLowerCase();
counts[s] = (counts[s] || 0) + 1;
});
Comment on lines +120 to 131
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 model output before accessing fields.

c.severity.toUpperCase() / toLowerCase() will throw if any item is malformed, and then neither inline review nor fallback comment is posted.

Proposed hardening
-            const comments = result.comments || [];
+            const comments = Array.isArray(result.comments) ? result.comments : [];
+            const normalized = comments
+              .filter(c => c && typeof c.path === 'string' && Number.isInteger(c.line) && typeof c.body === 'string')
+              .map(c => ({
+                path: c.path,
+                line: c.line,
+                body: c.body,
+                severity: typeof c.severity === 'string' ? c.severity.toLowerCase() : 'medium'
+              }));

-            if (comments.length === 0) {
+            if (normalized.length === 0) {
               await github.rest.issues.createComment({
                 issue_number: context.issue.number,
                 owner: context.repo.owner,
                 repo: context.repo.repo,
                 body: '## OpenAI Code Review\n\nNo issues found.'
               });
               return;
             }

-            const reviewComments = comments.map(c => ({
+            const reviewComments = normalized.map(c => ({
               path: c.path,
               line: c.line,
               body: `**[${c.severity.toUpperCase()}]** ${c.body}`
             }));

             const counts = {};
-            comments.forEach(c => {
+            normalized.forEach(c => {
               const s = c.severity.toLowerCase();
               counts[s] = (counts[s] || 0) + 1;
             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/openai-code-review.yml around lines 120 - 131, The mapping
and counting assume every comment has a string severity, which can throw when
calling c.severity.toUpperCase()/toLowerCase(); in the functions that build
reviewComments and counts, first normalize and validate severity (e.g., const
sev = (typeof c.severity === 'string' && c.severity) ? c.severity : 'info') then
use sev.toUpperCase()/sev.toLowerCase() when building objects and incrementing
counts; update the reviewComments creation and the comments.forEach counting
logic to use this validated sev variable so malformed items won't throw and a
sensible default (like "info"/"unknown") is used.

env:
OPENAI_REVIEW: ${{ env.OPENAI_REVIEW }}
const summary = Object.entries(counts)
.map(([s, n]) => `- **${s}**: ${n}`)
.join('\n');

try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
event: 'COMMENT',
body: `## OpenAI Code Review\n\nFound **${comments.length}** issues:\n${summary}`,
comments: reviewComments
});
Comment on lines +137 to +144
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

🧩 Analysis chain

🌐 Web query:

GitHub REST API POST /repos/owner/repo/pulls/pull_number/reviews comments side field required

💡 Result:

For POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews, each item in the comments array can locate the comment either by diff position or by line + side (and optionally start_line + start_side for multi-line ranges). GitHub documents side as part of the comments object for this endpoint. [1]

  • If you use position, then line/side (and start_line/start_side) are not required. [2]
  • If you use line, you should include side to indicate which side of the split diff the line is on (LEFT = deletions, RIGHT = additions/context). [2]
  • For multi-line comments, include start_line and start_side (and side applies to the end of the range). [2]

Allowed values: side (and start_side) are LEFT or RIGHT. [2]

Example (line-based):

{
  "event": "COMMENT",
  "comments": [
    {
      "path": "src/app.js",
      "body": "Nit: rename this variable",
      "line": 42,
      "side": "RIGHT"
    }
  ]
}

Example (position-based; no side needed):

{
  "event": "COMMENT",
  "comments": [
    {
      "path": "src/app.js",
      "body": "Comment at diff position",
      "position": 6
    }
  ]
}

Sources: GitHub REST API docs for “Create a review for a pull request” (comments object includes side). [1] GitHub REST API docs for “Create a review comment for a pull request” (explains when line/side vs position are required, and defines side). [2]


🏁 Script executed:

# Find and examine the actual workflow file
find .github/workflows -name "openai-code-review.yml" -type f

Repository: reeder32/teamexpense

Length of output: 104


🏁 Script executed:

# Also search for where reviewComments is defined to see the full context
rg -n "reviewComments" .github/workflows/ -A 5 -B 5

Repository: reeder32/teamexpense

Length of output: 2012


Add side field to inline comments to prevent repeated fallback failures.

When using line to locate comments in the GitHub REST API, the side field is required to indicate which side of the diff the line appears on. The current code omits side, causing inline comment creation to fail and fall back to a single review comment every time.

Suggested fix
             const reviewComments = comments.map(c => ({
               path: c.path,
               line: c.line,
+              side: 'RIGHT',
               body: `**[${c.severity.toUpperCase()}]** ${c.body}`
             }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/openai-code-review.yml around lines 137 - 144, The inline
review creation is failing because the per-comment objects in reviewComments
passed to github.rest.pulls.createReview are missing the required side property
when using line-based positioning; update the reviewComments construction so
each inline comment includes side (e.g., 'RIGHT' for new changes or compute from
the comment data) before calling github.rest.pulls.createReview, ensuring the
objects match GitHub's API shape expected by the createReview call.

} catch (err) {
// If inline comments fail (e.g. line not in diff), fall back to single comment
let body = `## OpenAI Code Review\n\nFound **${comments.length}** issues:\n${summary}\n\n`;
body += '| File | Line | Severity | Issue |\n|------|------|----------|-------|\n';
comments.forEach(c => {
body += `| \`${c.path}\` | ${c.line} | ${c.severity} | ${c.body} |\n`;
});
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
}
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");

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)
);
`);

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.

Wildcard CORS allows any origin to make authenticated requests, enabling CSRF attacks

Suggested change
app.use(cors());
app.use(cors({ origin: process.env.FRONTEND_URL || 'http://localhost:3000', credentials: true }));

app.use(express.json());
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] CORS is configured with a wildcard '*', allowing any origin to access resources. This can lead to security vulnerabilities. CORS should be limited to specific trusted domains.


app.use("/api/auth", authRoutes);
app.use("/api/expenses", expenseRoutes);
app.use("/api/reports", reportRoutes);
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] No rate limiting is implemented on any of the endpoints, exposing the application to brute-force attacks. Implement rate limiting to mitigate this risk.


// 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.

[CRITICAL] The error handler leaks stack traces in the response, which can expose sensitive application details. Stack traces should not be exposed in production environments.


// 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 +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.

Stack traces leaked to client in production expose internal implementation details

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: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message });
});

});
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.

Action required

5. Stack traces to client 🐞 Bug ⛨ Security

Global error handler returns err.stack to the client, leaking internals (paths, code structure)
and potentially sensitive data. This aids attackers and exposes implementation details in
production.
Agent Prompt
### Issue description
Stack traces are returned to clients.

### Issue Context
This is an information disclosure risk.

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

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


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.

Hardcoded JWT secret allows anyone with repository access to forge authentication tokens - use environment variable instead

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: Hardcoded JWT secret.

The secret "supersecretkey123" is committed to source control. Anyone with repo access can forge valid JWTs. Use an environment variable.

Required fix
-const JWT_SECRET = "supersecretkey123"; // BUG: Hardcoded JWT secret — should use environment variable
+const JWT_SECRET = process.env.JWT_SECRET;
+if (!JWT_SECRET) {
+  throw new Error("JWT_SECRET environment variable is required");
+}
🤖 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, Replace the hardcoded JWT secret
constant JWT_SECRET with a value read from an environment variable (e.g.,
process.env.JWT_SECRET) and ensure code throws or logs a clear error if the env
var is missing; update any functions that import/require JWT_SECRET (e.g., token
signing/verification logic) to use the new environment-driven value and add a
fallback check during module initialization to prevent starting without a
configured secret.

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. Hardcoded jwt secret 🐞 Bug ⛨ Security

JWT secret is hardcoded and exported from source, enabling token forgery if code leaks (and
increasing chance of accidental disclosure). This undermines all authentication/authorization.
Agent Prompt
### Issue description
JWT secret is hardcoded and exported.

### Issue Context
Anyone with access to the repo/artifacts can forge JWTs.

### Fix Focus Areas
- backend/src/middleware/auth.ts[4-4]
- backend/src/middleware/auth.ts[36-36]

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


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] JWT secrets should not be hardcoded directly in the source code. Use environment variables to secure sensitive information like JWT secrets.

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
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] The 'Bearer ' prefix is not validated in the authorization token, so any valid JWT token can pass, regardless of the prefix. Ensure that the token format is validated correctly.

const token = header.replace("Bearer ", "");
Comment on lines +16 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

.replace() accepts any string without checking format - missing validation that authorization header uses proper scheme prefix


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") {
Comment on lines +29 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Role verification uses JWT payload which users control - users can forge admin tokens by modifying the JWT payload before signing

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.

Action required

4. Admin trust in jwt 🐞 Bug ⛨ Security

Admin authorization trusts the role claim from the JWT payload instead of verifying it from the
database. Combined with a compromised/known secret, this enables privilege escalation to admin
endpoints.
Agent Prompt
### Issue description
Admin authorization relies on a role claim from the JWT instead of server-side state.

### Issue Context
Even with correct signature verification, role should not be accepted without DB verification.

### Fix Focus Areas
- backend/src/middleware/auth.ts[28-34]
- backend/src/routes/reports.ts[22-36]

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

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] The role is verified using the data from the JWT payload, which can be tampered by the user. Verify the user's role against the database to ensure proper access control.

}

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.

password can be empty string or single character - no minimum length or complexity validation before hashing


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
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] No expiration is set for JWTs, making them valid indefinitely. It's safer to include an expiration time to force users to refresh their tokens periodically.

// BUG: No token expiration — JWTs are valid forever
);
Comment on lines +32 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.

JWTs valid forever without expiration - compromised tokens remain valid indefinitely

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.

Action required

3. No jwt expiry 🐞 Bug ⛨ Security

JWTs are issued without an expiration, so stolen tokens remain valid indefinitely. This increases
blast radius for XSS/token leakage and prevents forced logout.
Agent Prompt
### Issue description
JWTs are created without expiration.

### Issue Context
Stolen tokens remain usable forever.

### Fix Focus Areas
- backend/src/routes/auth.ts[29-34]
- backend/src/routes/auth.ts[65-70]

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


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
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] No minimum password length or complexity check is enforced, making it vulnerable to weak password usage. Enforce a password policy for better security.

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

Choose a reason for hiding this comment

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

SQL injection vulnerability - email is interpolated directly into query string, allowing attackers to bypass authentication

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 +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, allowing attackers to bypass authentication or extract data. An attacker can log in as any user with: ' OR '1'='1' --.

Required fix: use 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;
📝 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 in the auth route
is vulnerable because it interpolates the email into db.prepare; replace the
string-interpolated query with a parameterized one (e.g., const userStmt =
db.prepare('SELECT * FROM users WHERE email = ?'); const user =
userStmt.get(email);), using db.prepare and .get with the email as a parameter
instead of embedding the email variable; update any surrounding code that
expects the old "user" variable accordingly.


Comment on lines +52 to +54
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

/api/auth/login interpolates untrusted email directly into SQL, allowing attackers to bypass
authentication or extract data. This is in the primary auth path, so impact is critical.
Agent Prompt
### Issue description
`/api/auth/login` is vulnerable to SQL injection because it interpolates `email` into the query string.

### Issue Context
The code uses better-sqlite3 which supports prepared statements with positional parameters.

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

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

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

Choose a reason for hiding this comment

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

different error messages for "email not found" vs "wrong password" enables attacker to enumerate valid email addresses in the system

}

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] Potential SQL injection vulnerability as user input is directly interpolated into the SQL query string. This needs to be changed to use parameterized queries to avoid SQL injection attacks.

const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(401).json({ error: "Incorrect password" });
}

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] The error message reveals if an email address exists in the database, which can be exploited for user enumeration.

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