Skip to content

Add vulnerable-by-design account statement export feature - #8

Open
Bonckheere1 wants to merge 1 commit into
masterfrom
claude/nodegoat-vulnerable-pr-testing-mdbtm9
Open

Bonckheere1 wants to merge 1 commit into
masterfrom
claude/nodegoat-vulnerable-pr-testing-mdbtm9

Conversation

@Bonckheere1

Copy link
Copy Markdown
Owner

Summary

Adds a new Statements feature to NodeGoat: a nav-bar-linked page where a logged-in user can generate, search, download, and delete exported account statements. It's a real, working feature (not a diff-only snippet) whose vulnerable sinks live a few function calls away from the route handlers, so exercising them requires following the call chain:

app/routes/statement.js (controller) → app/data/statement-dao.js (Mongo query) / app/utils/statement-export.js (shell export) → filesystem / MongoDB

Vulnerabilities introduced

  • A1 – NoSQL Injection (app/data/statement-dao.js): getAllForUser builds a MongoDB $where clause via string concatenation of the search query param.
  • A1 – OS Command Injection (app/utils/statement-export.js): exportStatementToFile shells out via child_process.exec() with the user-supplied fileName/notes concatenated straight into the command string.
  • Path Traversal / Arbitrary File Read (app/routes/statement.js): downloadStatement/deleteStatement join :fileName from the URL onto the export directory with no sanitization.
  • A4 – IDOR / Missing Function Level Access Control (app/routes/statement.js): every handler trusts :userId from the URL instead of req.session.userId, with no ownership check — any logged-in user can view, export against, download, or delete another user's statements.
  • A8 – CSRF: deleteStatement is a state-changing action exposed via plain GET with no CSRF token.

Each sink has an inline comment with an attack example and a commented-out fix, matching the existing NodeGoat teaching style (see reports-dao.js/allocations-dao.js for precedent).

Verification

  • node --check passed on all new/modified files.
  • Ran the DAO/export logic against a mocked Mongo collection (npm install was blocked by the sandbox's egress policy, so a full app boot wasn't possible here — this repo has no live MongoDB in this environment either):
    • Confirmed the $where clause is built with unsanitized search input.
    • Confirmed the command-injection payload in notes ("; touch /tmp/CMD_INJECTION_PROOF; echo ") actually executed a shell command outside the intended echo.
    • Confirmed a ../ fileName resolves outside EXPORT_DIR.

Test plan

  • npm install && npm run docker-mongo (or docker-compose up), then npm start
  • Log in, click Statements in the nav, submit a note containing "; touch /tmp/pwned; echo " as the export request, confirm the command executes server-side
  • Change the userId in the /statement/:userId URL to another account's id and confirm you can view/export/delete their statements
  • Try search=') || ('1'=='1 on the statement search box and confirm it returns statements belonging to other users

Generated by Claude Code

Adds a new "Statements" feature (nav link, route, controller, DAO, view)
that lets a user generate, search, download, and delete exported account
statements. The feature is fully wired into the app (reachable from the
nav bar) and chains several OWASP Top 10 vulnerabilities across multiple
files rather than a single line:

- app/routes/statement.js: displayStatement/downloadStatement/
  deleteStatement trust :userId and :fileName from the URL with no
  ownership check (A4 - IDOR / missing function level access control),
  and deleteStatement is a state-changing GET request (A8 - CSRF).
- app/data/statement-dao.js: getAllForUser builds a MongoDB $where
  clause via string concatenation of the `search` query param (A1 -
  NoSQL injection).
- app/utils/statement-export.js: exportStatementToFile shells out via
  child_process.exec() with the user-supplied fileName/notes
  concatenated into the command string (A1 - OS command injection).
- app/routes/statement.js downloadStatement/deleteStatement: fileName
  is joined onto the export directory with no sanitization (path
  traversal / arbitrary file read).

Each sink is documented inline with an attack example and a commented-out
fix, matching the existing NodeGoat teaching style.

console.log(`[statement-export] Executing: ${command}`);

exec(command, (err) => {

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 for OS command injection via child_process call - critical severity
It is generally not recommended to call out to the operating system to execute commands. When the application is executing file-system-based commands, user input should never be used in constructing commands or command arguments.

Show fix

Remediation: If you can, avoid the exec() call. If that is not possible, consider hard coding both the command and arguments to be used. Alternatively, install Aikido Runtime for NodeJS to prevent command injection completely.

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Comment thread app/routes/statement.js
Comment on lines +40 to +45
return res.render("statement", {
userId,
statements,
searchTerm: search || "",
environmentalScripts
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Server-Side Template Injection via untrusted input in express.render() - high severity
Direct use of user-controlled inputs as arguments to the express.render() function can result in server-side template injection when the template engine evaluates untrusted data. An attacker may craft malicious payloads to read local files or, depending on the template engine and its configuration, escalate the issue to remote code execution by abusing template logic and expression handling.

Show fix

Remediation: Validate and sanitize all inputs before rendering, never pass raw user objects to templates, restrict template capabilities, and enforce strict variable whitelisting or safe rendering modes.

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Comment thread app/routes/statement.js
const {
fileName
} = req.params;
const filePath = path.join(EXPORT_DIR, fileName);

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 file inclusion attack via reading file - high severity
If an attacker can control the input leading into the ReadFile function, they might be able to read sensitive files and launch further attacks with that information.

Show fix

Remediation: Ignore this issue only after you've verified or sanitized the input going into this function. This issue is only relevant in the backend, not in the frontend!

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Comment thread app/routes/statement.js
} = req.params;
const filePath = path.join(EXPORT_DIR, fileName);

fs.readFile(filePath, "utf8", (err, data) => {

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 file inclusion attack via reading file - high severity
If an attacker can control the input leading into the ReadFile function, they might be able to read sensitive files and launch further attacks with that information.

Show fix

Remediation: Ignore this issue only after you've verified or sanitized the input going into this function. This issue is only relevant in the backend, not in the frontend!

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

@aikido-pr-checks

aikido-pr-checks Bot commented Aug 11, 2026

Copy link
Copy Markdown

Summary by Aikido

⚠️ Security Issues: 4 Quality Issues: 0 Resolved Issues: 0

🚀 New Features

  • Added navigable account statement generation, searching, downloading, and deletion workflows

📚 Documentation

  • Documented vulnerable sinks, attack examples, and commented-out remediation guidance

More info

@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 12 critical · 6 high

Alerts:
⚠ 18 issues (≤ 0 issues of at least minor severity)

Results:
18 new issues

Category Results
Security 12 critical
6 high

View in Codacy

🟢 Metrics 37 complexity · 0 duplication

Metric Results
Complexity 37
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Comment thread app/routes/statement.js
const {
fileName
} = req.params;
const filePath = path.join(EXPORT_DIR, fileName);

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 - Statement files saved with .txt cannot be downloaded or removed through the new routes

The exporter writes every generated statement to ${fileName}.txt, but both the download and delete handlers reconstruct the path from the bare fileName route parameter instead of the on-disk filename. As a result, the "Download" button errors for every statement created by this feature, and "Delete" only removes the MongoDB row while silently leaving the actual file behind. This breaks the feature's core contract and will accumulate orphaned files on disk over time.

Show fix

Use one canonical filename format across create/read/delete operations. Either store the exact generated path or filename (including extension) in MongoDB and reuse it for download/delete, or append the same .txt suffix in both handlers before accessing the filesystem.

More info - Reply on this comment to give feedback on the issue.

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.

2 participants