-
Notifications
You must be signed in to change notification settings - Fork 0
Add app source code for AI code review comparison #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||
| }).encode() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error path currently blocks the reporting step. Line 85 exits non-zero, so the next step won’t run under default Proposed fix - name: Post Inline Review Comments
+ if: always()
uses: actions/github-script@v7Also applies to: 91-91 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate model output before accessing fields.
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 |
||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: For
Allowed values: Example (line-based): {
"event": "COMMENT",
"comments": [
{
"path": "src/app.js",
"body": "Nit: rename this variable",
"line": 42,
"side": "RIGHT"
}
]
}Example (position-based; no {
"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 🏁 Script executed: # Find and examine the actual workflow file
find .github/workflows -name "openai-code-review.yml" -type fRepository: 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 5Repository: reeder32/teamexpense Length of output: 2012 Add When using 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 |
||||||||||||||||||||||||
| } 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 | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| 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; |
| 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()); | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(express.json()); | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||
| }); | ||||||||||||||||
|
Comment on lines
+21
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 5. Stack traces to client 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
|
||||||||||||||||
|
|
||||||||||||||||
| app.listen(PORT, () => { | ||||||||||||||||
| console.log(`Server running on port ${PORT}`); | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| export default app; | ||||||||||||||||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Hardcoded JWT secret. The secret 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 AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Hardcoded jwt secret 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
|
||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Admin trust in jwt 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }; | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. No jwt expiry 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
|
||||||||||||
|
|
||||||||||||
| 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 | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Comment on lines
+52
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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: 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
|
Comment on lines
+52
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Sqli in login /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
|
||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" }); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
anthropics claude-code-action permission requirements id-token💡 Result:
id-token: writeis not inherently required byanthropics/claude-code-actionanymore (it was removed in v1.0.6 along with adding agithub_tokeninput). [3]You do need
permissions: id-token: writein 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
id-token: writeneeded (just set the GitHub token permissions you need for repo operations).permissions: id-token: write(in addition to repo permissions likecontents,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-actiondefines its GitHub Action metadata inaction.ymlat the repo root. It’s a composite action (runs steps on the runner), installs/uses Bun, installs dependencies, then runsbun run .../src/entrypoints/run.ts. [1][2]Key parts of
action.ymlyou’ll likely care about: [2]name: "Claude Code Action v1.0"trigger_phrase(default@claude)label_trigger(defaultclaude)prompt(instructions for Claude; default empty)claude_args(extra CLI args; default empty)anthropic_api_key,claude_code_oauth_token, plus togglesuse_bedrock,use_vertex,use_foundrygithub_token(optional; used if not using the GitHub App token flow)execution_file,branch_name,github_token,structured_output,session_idruns: using: "composite"with steps including Bun setup + “Run Claude Code Action”Anthropic’s docs show basic workflow usage like:
uses: anthropics/claude-code-action@v1withanthropic_api_key,prompt, and optionalclaude_args. [3]Sources
[1] anthropics/claude-code-action (repo) — https://github.com/anthropics/claude-code-action
[2]
action.yml— https://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: writeis 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 (viaACTIONS_ID_TOKEN_REQUEST_URL/ACTIONS_ID_TOKEN_REQUEST_TOKEN) or the Actions toolkitgetIDToken()—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:
(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: writein the caller workflow/job that invokes it. [1][2]Not required if you’re not using OIDC (e.g., only using
GITHUB_TOKENAPI access, repo checkout, artifacts, etc.). [1]🏁 Script executed:
Repository: reeder32/teamexpense
Length of output: 2340
Remove the unnecessary
id-token: writepermission.The workflow uses only
anthropic_api_keyfor direct Anthropic API authentication and does not configure any cloud provider OIDC authentication (Bedrock, Vertex, or Foundry). Theid-token: writepermission 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