Sample report
A real report, exactly as delivered.
Below is an actual $248 analysis (Automated code scan + AI architecture review), rendered with the exact same component a customer sees on their own report page. Nothing here was rewritten for this page.
What you're looking at
This report is real and unedited — we ran our pipeline against badapp, a small demo application we built specifically to have problems, so we can show you exactly what a report looks like without putting anyone’s actual code on display. That’s also why six findings below are marked critical: badapp was built to have them.
Your report
Here’s what we found.
Summary
Back to topThere's a lot working well here, and 46 items stand out as worth attention soon — each one below has a clear next step.
What’s strong
- Your upload passed our safety checks before any analysis began, so nothing unexpected was in the archive.
- Every check we set out to run finished, so this covers the code you sent us in full.
- This review ran against a fixed, recorded set of checks, so the same run can be repeated later and compared.
- Every item below comes with a concrete next step — they are all known, fixable issues.
Worth your attention
- 46 items are worth handling soon — they are listed first below.
- 36 further items are worth tidying up when you have time.
- 1Info
- 14Low
- 21Medium
- 32High
- 14Critical
Findings
- CriticalAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/db/migrations/0002_add_payments.sql (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is the kind of thing worth handling first.
See technical detail
badapp/db/migrations/0002_add_payments.sql:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/db/migrations/0002_add_payments.sql:1. Rule identifier: architecture.security.plaintext-card-data-storage.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The payments table declares card_number, card_cvv and card_expiry as plain text, and paymentService.charge inserts those values directly. Storing CVV after authorization is prohibited under PCI DSS under any circumstances, and storing PAN in cleartext pulls the entire database, its backups and anything with DB credentials into card-data scope. The public-read backups bucket in the Terraform makes the blast radius worse. Reviewed: badapp/db/migrations/0002_add_payments.sql (lines 1-16) — quoted from the scanned source -- Payments. Storing PAN + CVV is the point: an architecture/compliance finding that no -- SAST rule will flag, because nothing here is syntactically wrong. CREATE TABLE IF NOT EXISTS "payments" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "order_id" uuid NOT NULL REFERENCES "orders"("id"), "amount_cents" integer NOT NULL, "currency" varchar(3) NOT NULL DEFAULT 'usd', "card_number" text NOT NULL, "card_cvv" text NOT NULL, "card_expiry" text NOT NULL ); CREATE UNIQUE INDEX "payments_order_idx" ON "payments" ("order_id"); ALTER TABLE "orders" ADD COLUMN "paid_at" timestamptz; Suggested remediation: Stop persisting card data: delegate capture to a payment processor and store only an opaque token plus last four digits and brand. Drop card_cvv immediately and plan a migration to remove card_number. - CriticalAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/infra/main.tf (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is the kind of thing worth handling first.
See technical detail
badapp/infra/main.tf:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/infra/main.tf:1. Rule identifier: architecture.security.public-read-object-storage.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: aws_s3_bucket.backups sets acl = "public-read". Given the schema stores plaintext card data and password hashes, a world-readable backup bucket exposes the entire customer dataset to anyone who can enumerate or guess the object keys. There is no block-public-access configuration alongside it. Reviewed: badapp/infra/main.tf (lines 1-14) — quoted from the scanned source resource "aws_s3_bucket" "backups" { bucket = "badapp-backups" acl = "public-read" } resource "aws_security_group" "web" { ingress { from_port = 0 to_port = 65535 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } Suggested remediation: Remove the public-read ACL, set the bucket to private, and add an aws_s3_bucket_public_access_block with all four flags enabled plus server-side encryption. - CriticalAI review
A structural point about how your app is put together
- What we found
- Looking at the shape of your project as a whole — how the pieces fit together and talk to each other — we noted something worth thinking about in badapp/src/app.js (line 1).
- What it means
- This is about design rather than a specific bug: choices like this one tend to decide how easy the app is to change, and how well it holds up as it grows.
- What to do
- Read the detail below with whoever knows the codebase best and decide together whether it's worth adjusting now or noting for later — it's a trade-off, not an emergency. This is the kind of thing worth handling first.
See technical detail
badapp/src/app.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/app.js:1. Rule identifier: architecture.security.unauthenticated-admin-route.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: app.js mounts adminRoutes at /admin with no auth middleware, and admin.js defines no per-route guard. The routes behind it include a shell-executing /ping and a /refund that writes payment rows. The users table has an is_admin column and the project depends on jsonwebtoken, so an authorization concept exists in the data model but is never enforced in the request path. Reviewed: badapp/src/app.js (lines 1-15) — quoted from the scanned source const express = require("express"); const publicRoutes = require("./routes/public"); const userRoutes = require("./routes/users"); const adminRoutes = require("./routes/admin"); const { log } = require("./lib/logger"); const app = express(); app.use("/", publicRoutes); app.use("/", userRoutes); // ARCHITECTURE SMELL: admin surface mounted with no authentication middleware at all. app.use("/admin", adminRoutes); app.listen(3000, () => log("listening on 3000")); Suggested remediation: Introduce an authentication middleware (verify a signed token, load the user, require is_admin) and apply it at the app.use('/admin', ...) mount point before any admin handler is registered. - CriticalAI review
Visitor input is trusted directly in a database query
Effort: About an hour- What we found
- In badapp/src/db/queries.js (line 1), text that comes from whoever is using your app is dropped straight into a database instruction instead of being handled safely first.
- What it means
- A determined visitor could craft input that reads or changes data they should never be able to reach.
- What to do
- Use the database's built-in placeholders so anything a visitor types is always treated as data, never as an instruction. This is the kind of thing worth handling first.
See technical detail
badapp/src/db/queries.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/db/queries.js:1. Rule identifier: architecture.security.sql-injection-string-concatenation.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: findUser, findOrders and findReviews interpolate their arguments into SQL with + and no escaping, and they are fed directly from req.query.id / req.query.userId by the /user and /orders routes. The connection authenticates as the MySQL root user, so a successful injection has full database privileges including reads of the payments table. Reviewed: badapp/src/db/queries.js (lines 1-13) — quoted from the scanned source const { connect } = require("./client"); // SQL injection: string concatenation straight into the query. exports.findUser = (id, cb) => connect().query("SELECT * FROM users WHERE id = '" + id + "'", cb); exports.findOrders = (userId, cb) => connect().query("SELECT * FROM orders WHERE user_id = '" + userId + "'", cb); // ARCHITECTURE SMELL: queries a `reviews` table that no migration creates (schema drift). exports.findReviews = (productId, cb) => connect().query("SELECT * FROM reviews WHERE product_id = '" + productId + "'", cb); Suggested remediation: Switch every query to parameter placeholders (mysql's query(sql, [params]) form) and forbid string-built SQL in the data layer via review or a lint rule. - CriticalAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/src/routes/admin.js (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is the kind of thing worth handling first.
See technical detail
badapp/src/routes/admin.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/routes/admin.js:1. Rule identifier: architecture.security.command-injection-shell-exec.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: exec() runs its argument through a shell, and req.query.host is concatenated directly into it. A host value containing shell metacharacters runs additional commands in the container. Combined with the missing authentication on the /admin router, this is an unauthenticated shell. Reviewed: badapp/src/routes/admin.js (lines 1-19) — quoted from the scanned source const express = require("express"); const { exec } = require("child_process"); const paymentService = require("../services/paymentService"); const router = express.Router(); // NO AUTH ON ANY OF THESE. The architecture finding is the missing boundary, not the syntax. // shell command built from user input -> command injection router.get("/ping", (req, res) => exec("ping -c 1 " + req.query.host, (err, stdout) => res.send(stdout)), ); router.post("/refund", (req, res) => paymentService.charge(req.query.orderId, { number: "[secret redacted]", cvv: "123", expiry: "12/30" }, (e) => res.json({ ok: !e })), ); module.exports = router; Suggested remediation: Replace exec with execFile('ping', ['-c','1', host]) and validate host against a strict hostname/IP pattern before use; then put the route behind authentication. - CriticalAI review
A structural point about how your app is put together
- What we found
- Looking at the shape of your project as a whole — how the pieces fit together and talk to each other — we noted something worth thinking about in badapp/src/routes/users.js (line 1).
- What it means
- This is about design rather than a specific bug: choices like this one tend to decide how easy the app is to change, and how well it holds up as it grows.
- What to do
- Read the detail below with whoever knows the codebase best and decide together whether it's worth adjusting now or noting for later — it's a trade-off, not an emergency. This is the kind of thing worth handling first.
See technical detail
badapp/src/routes/users.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/routes/users.js:1. Rule identifier: architecture.security.eval-on-user-input.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The /calc handler evaluates req.query.expr with eval() and returns the result. Any caller controls the string that is evaluated in the Node process, which means arbitrary JavaScript execution with the privileges of the app (which, per the Dockerfile, runs as root). This is full remote code execution on the web tier, not an input-validation nit. Reviewed: badapp/src/routes/users.js (lines 1-16) — quoted from the scanned source const express = require("express"); const queries = require("../db/queries"); const orderService = require("../services/orderService"); const router = express.Router(); router.get("/user", (req, res) => queries.findUser(req.query.id, (e, rows) => res.json(rows))); router.get("/orders", (req, res) => orderService.ordersFor(req.query.userId, (e, rows) => res.json(rows)), ); // eval on user input -> code injection router.get("/calc", (req, res) => res.send(String(eval(req.query.expr)))); module.exports = router; Suggested remediation: Delete the /calc route. If expression evaluation is genuinely a product requirement, replace eval with a sandboxed, non-Turing-complete expression parser and an allowlisted grammar. - CriticalAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/worker/db.py (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is the kind of thing worth handling first.
See technical detail
badapp/worker/db.py:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/worker/db.py:1. Rule identifier: architecture.security.command-injection-shell-exec.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: worker/db.py concatenates a path into a shell command with shell=True, and run() calls eval() on its argument. Whatever feeds these functions becomes a code-execution channel into the worker container, which shares the same image and network as the web service. Reviewed: badapp/worker/db.py (lines 1-32) — quoted from the scanned source import os import sqlite3 import subprocess DSN = os.environ["DATABASE_URL"] def lookup(user_id): conn = sqlite3.connect("app.db") # SQL injection: query built by string formatting return conn.execute("SELECT * FROM users WHERE id = '%s'" % user_id).fetchall() def mark_paid(order_id): # ARCHITECTURE SMELL: a second, independent writer to `orders` in another language, # with no shared schema ownership and no migration of its own. conn = sqlite3.connect("app.db") conn.execute("UPDATE orders SET status = 'paid' WHERE id = '%s'" % order_id) def backup(path): # command injection: shell=True with interpolated input subprocess.call("tar czf backup.tgz " + path, shell=True) def run(code): # arbitrary code execution return eval(code) API_KEY = "[secret redacted]" Suggested remediation: Replace subprocess.call("tar czf ... " + path, shell=True) with a list-argument call and shell=False, and delete run(); if dynamic behaviour is needed, dispatch through an explicit map of allowed operations. - HighAI review
A structural point about how your app is put together
- What we found
- Looking at the shape of your project as a whole — how the pieces fit together and talk to each other — we noted something worth thinking about in badapp/.github/workflows/ci.yml (line 1).
- What it means
- This is about design rather than a specific bug: choices like this one tend to decide how easy the app is to change, and how well it holds up as it grows.
- What to do
- Read the detail below with whoever knows the codebase best and decide together whether it's worth adjusting now or noting for later — it's a trade-off, not an emergency. This is worth handling soon.
See technical detail
badapp/.github/workflows/ci.yml:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/.github/workflows/ci.yml:1. Rule identifier: architecture.maintainability.no-automated-tests.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The scan reports testFileCount 0 across 24 files, and the CI workflow runs npm install && npm test as its sole step. With no tests present that step either fails or passes vacuously; either way the pipeline provides no signal, and there is nothing preventing regressions in the payment and query paths. Reviewed: badapp/.github/workflows/ci.yml (lines 1-9) — quoted from the scanned source name: ci on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: npm install && npm test Suggested remediation: Add integration tests for the routes and the query layer (starting with /orders and paymentService), and make CI fail on missing tests rather than treating an empty suite as a pass. - HighAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/db/migrations/0001_init.sql (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth handling soon.
See technical detail
badapp/db/migrations/0001_init.sql:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/db/migrations/0001_init.sql:1. Rule identifier: architecture.maintainability.schema-dialect-mismatch.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The migrations use uuid columns, gen_random_uuid(), timestamptz and quoted identifiers — PostgreSQL syntax — while the application uses the npm mysql driver against a mysql:5.7 container with a mysql:// DSN. These migrations cannot create the schema the running code queries, so the declared data model and the deployed one will diverge from day one. Reviewed: badapp/db/migrations/0001_init.sql (lines 1-27) — quoted from the scanned source -- Core schema. Deliberately stores card data in plaintext (design finding, not a SAST one). CREATE TABLE IF NOT EXISTS "users" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "email" text NOT NULL UNIQUE, "password_hash" text NOT NULL, "is_admin" boolean NOT NULL DEFAULT false, "created_at" timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS "products" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "sku" varchar(64) NOT NULL UNIQUE, "name" text NOT NULL, "price_cents" integer NOT NULL, CONSTRAINT "products_price_positive" CHECK ("price_cents" > 0) ); CREATE TABLE IF NOT EXISTS "orders" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL REFERENCES "users"("id"), "product_id" uuid NOT NULL REFERENCES "products"("id"), "quantity" integer NOT NULL DEFAULT 1, "status" text NOT NULL DEFAULT 'pending' ); CREATE INDEX "orders_user_idx" ON "orders" ("user_id"); Suggested remediation: Pick one engine. If MySQL is the target, rewrite the migrations in MySQL syntax (CHAR(36)/BINARY(16) ids, TIMESTAMP, backtick quoting) and run them in CI against the same image used in compose. - HighAI review
A structural point about how your app is put together
- What we found
- Looking at the shape of your project as a whole — how the pieces fit together and talk to each other — we noted something worth thinking about in badapp/infra/main.tf (line 1).
- What it means
- This is about design rather than a specific bug: choices like this one tend to decide how easy the app is to change, and how well it holds up as it grows.
- What to do
- Read the detail below with whoever knows the codebase best and decide together whether it's worth adjusting now or noting for later — it's a trade-off, not an emergency. This is worth handling soon.
See technical detail
badapp/infra/main.tf:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/infra/main.tf:1. Rule identifier: architecture.security.security-group-allows-all-ingress.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: aws_security_group.web has a single ingress rule covering ports 0-65535 from any source address. That exposes any service that happens to bind on the instance — database, admin ports, debug listeners — rather than just the application port, and removes the network layer as a compensating control for the auth gaps above. Reviewed: badapp/infra/main.tf (lines 1-14) — quoted from the scanned source resource "aws_s3_bucket" "backups" { bucket = "badapp-backups" acl = "public-read" } resource "aws_security_group" "web" { ingress { from_port = 0 to_port = 65535 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } Suggested remediation: Narrow the ingress rule to the specific application port from the load balancer's security group, and add explicit rules only for ports that must be reachable. - HighAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/src/config.js (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth handling soon.
See technical detail
badapp/src/config.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/config.js:1. Rule identifier: architecture.security.credentials-committed-in-source.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: src/config.js exports awsAccessKeyId, awsSecretAccessKey, apiToken, adminPassword and jwtSecret as literals in a source file, and db/client.js consumes adminPassword as the database password. Whatever the current values are, this design puts credentials in version control and in every image layer, makes rotation a code deploy, and prevents per-environment separation. A source comment asserts the values are decoys; that claim cannot be verified from the code and should not be relied on. Reviewed: badapp/src/config.js (lines 1-9) — quoted from the scanned source // Hardcoded credentials — scanner bait, not real. module.exports = { awsAccessKeyId: "[secret redacted]", awsSecretAccessKey: "[secret redacted]", apiToken: "[secret redacted]", adminPassword: "[secret redacted]", jwtSecret: "[secret redacted]", }; Suggested remediation: Change config.js to read from process.env (the repo already declares names such as AWS_ACCESS_KEY_ID, JWT_SIGNING_SECRET, DATABASE_URL), fail fast when a variable is missing, and rotate every value that has ever been committed. - HighAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/src/db/client.js (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth handling soon.
See technical detail
badapp/src/db/client.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/db/client.js:1. Rule identifier: architecture.security.database-superuser-in-application.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: db/client.js hardcodes user "root" and passes config.adminPassword as the database password, and docker-compose sets DATABASE_URL to a root DSN. Any SQL injection therefore executes with full DDL and cross-database privileges, and one secret serves two unrelated trust boundaries. Reviewed: badapp/src/db/client.js (lines 1-12) — quoted from the scanned source const mysql = require("mysql"); const config = require("../config"); // ARCHITECTURE SMELL: a new connection per call, credentials from a checked-in module. exports.connect = () => mysql.createConnection({ host: "db", user: "root", password: config.adminPassword, database: "badapp", }); Suggested remediation: Create a dedicated application database user with DML-only grants on the badapp schema, give it its own secret supplied via the environment, and stop reusing adminPassword for the datastore. - HighAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/src/db/client.js (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth handling soon.
See technical detail
badapp/src/db/client.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/db/client.js:1. Rule identifier: architecture.performance.connection-per-query-no-pooling.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: client.connect() calls mysql.createConnection on each invocation and no caller ends the connection. Under load this means a TCP handshake plus MySQL auth per request, and connections accumulate until the server's max_connections is hit, at which point all queries start failing. The mysql driver ships a pool API that is going unused. Reviewed: badapp/src/db/client.js (lines 1-12) — quoted from the scanned source const mysql = require("mysql"); const config = require("../config"); // ARCHITECTURE SMELL: a new connection per call, credentials from a checked-in module. exports.connect = () => mysql.createConnection({ host: "db", user: "root", password: config.adminPassword, database: "badapp", }); Suggested remediation: Replace createConnection with a single module-level mysql.createPool and have the query layer borrow and release connections, or call pool.query directly. - HighAI review
Text from a visitor is shown on the page without being made safe
Effort: About an hour- What we found
- Content supplied by whoever is using your app is rendered back onto the page in badapp/src/routes/public.js (line 1) without being neutralized first.
- What it means
- Someone could sneak instructions into that content and have them run in another person's browser — for example to hijack their signed-in session.
- What to do
- Let your framework escape the value when it renders, and avoid the 'insert raw HTML' escape hatches unless the content is something you generated yourself. This is worth handling soon.
See technical detail
badapp/src/routes/public.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/routes/public.js:1. Rule identifier: architecture.security.reflected-xss.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The handler concatenates req.query.name into an HTML response and sends it with Express's default text/html content type, so attacker-supplied markup and script execute in the victim's browser on the application origin. There is no CSP or output-encoding layer anywhere in the app. Reviewed: badapp/src/routes/public.js (lines 1-10) — quoted from the scanned source const express = require("express"); const router = express.Router(); router.get("/health", (req, res) => res.json({ ok: true })); // reflected user input -> XSS router.get("/hello", (req, res) => res.send("<h1>Hello " + req.query.name + "</h1>")); module.exports = router; Suggested remediation: HTML-escape the value before interpolation (or render via a template engine with auto-escaping) and add a restrictive Content-Security-Policy header. - HighAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/src/routes/users.js (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth handling soon.
See technical detail
badapp/src/routes/users.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/routes/users.js:1. Rule identifier: architecture.security.missing-authorization-on-data-access.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: Both routes take the identifier straight from req.query and return the rows as JSON with no session, no ownership check and no field filtering. findUser does SELECT *, so password_hash and is_admin are serialised to the caller. This is an insecure direct object reference plus over-exposure of sensitive columns. Reviewed: badapp/src/routes/users.js (lines 1-16) — quoted from the scanned source const express = require("express"); const queries = require("../db/queries"); const orderService = require("../services/orderService"); const router = express.Router(); router.get("/user", (req, res) => queries.findUser(req.query.id, (e, rows) => res.json(rows))); router.get("/orders", (req, res) => orderService.ordersFor(req.query.userId, (e, rows) => res.json(rows)), ); // eval on user input -> code injection router.get("/calc", (req, res) => res.send(String(eval(req.query.expr)))); module.exports = router; Suggested remediation: Derive the acting user from an authenticated session/token, scope the query to that user (or require an admin role), and project only the columns the client needs instead of SELECT *. - HighAI review
Visitor input is trusted directly in a database query
Effort: About an hour- What we found
- In badapp/src/services/paymentService.js (line 1), text that comes from whoever is using your app is dropped straight into a database instruction instead of being handled safely first.
- What it means
- A determined visitor could craft input that reads or changes data they should never be able to reach.
- What to do
- Use the database's built-in placeholders so anything a visitor types is always treated as data, never as an instruction. This is worth handling soon.
See technical detail
badapp/src/services/paymentService.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/services/paymentService.js:1. Rule identifier: architecture.security.sql-injection-string-concatenation.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: paymentService.charge builds an INSERT by concatenating orderId and the card number, CVV and expiry into the statement text. Besides the injection risk, string-built SQL containing PAN data tends to end up verbatim in driver error messages and query logs. Reviewed: badapp/src/services/paymentService.js (lines 1-11) — quoted from the scanned source const { connect } = require("../db/client"); // ARCHITECTURE SMELL: a service reaching past the query layer into the driver directly, // AND persisting full card data (see 0002_add_payments.sql). exports.charge = (orderId, card, cb) => connect().query( "INSERT INTO payments (order_id, amount_cents, card_number, card_cvv, card_expiry) VALUES ('" + orderId + "', 100, '" + card.number + "', '" + card.cvv + "', '" + card.expiry + "')", cb, ); Suggested remediation: Parameterise the insert, and route it through the shared query layer rather than calling connect().query directly from a service. - HighAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/worker/db.py (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth handling soon.
See technical detail
badapp/worker/db.py:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/worker/db.py:1. Rule identifier: architecture.security.sql-injection-string-concatenation.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: lookup() and mark_paid() interpolate user_id and order_id into SQL strings with %s formatting. sqlite3 accepts parameters natively, so this is gratuitous injection exposure in a component that mutates order payment status. Reviewed: badapp/worker/db.py (lines 1-32) — quoted from the scanned source import os import sqlite3 import subprocess DSN = os.environ["DATABASE_URL"] def lookup(user_id): conn = sqlite3.connect("app.db") # SQL injection: query built by string formatting return conn.execute("SELECT * FROM users WHERE id = '%s'" % user_id).fetchall() def mark_paid(order_id): # ARCHITECTURE SMELL: a second, independent writer to `orders` in another language, # with no shared schema ownership and no migration of its own. conn = sqlite3.connect("app.db") conn.execute("UPDATE orders SET status = 'paid' WHERE id = '%s'" % order_id) def backup(path): # command injection: shell=True with interpolated input subprocess.call("tar czf backup.tgz " + path, shell=True) def run(code): # arbitrary code execution return eval(code) API_KEY = "[secret redacted]" Suggested remediation: Use parameterised execute calls, e.g. conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)), throughout worker/db.py. - HighAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/worker/db.py (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth handling soon.
See technical detail
badapp/worker/db.py:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/worker/db.py:1. Rule identifier: architecture.maintainability.divergent-datastores-for-same-tables.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: worker/db.py reads DATABASE_URL into DSN but then ignores it and connects to sqlite3 "app.db", updating orders.status to 'paid'. Two services claim ownership of the same logical table in two different databases, and the SQLite file is per-container and ephemeral, so payment status written by the worker is invisible to the web tier and lost on restart. Reviewed: badapp/worker/db.py (lines 1-32) — quoted from the scanned source import os import sqlite3 import subprocess DSN = os.environ["DATABASE_URL"] def lookup(user_id): conn = sqlite3.connect("app.db") # SQL injection: query built by string formatting return conn.execute("SELECT * FROM users WHERE id = '%s'" % user_id).fetchall() def mark_paid(order_id): # ARCHITECTURE SMELL: a second, independent writer to `orders` in another language, # with no shared schema ownership and no migration of its own. conn = sqlite3.connect("app.db") conn.execute("UPDATE orders SET status = 'paid' WHERE id = '%s'" % order_id) def backup(path): # command injection: shell=True with interpolated input subprocess.call("tar czf backup.tgz " + path, shell=True) def run(code): # arbitrary code execution return eval(code) API_KEY = "[secret redacted]" Suggested remediation: Make the worker connect using DSN through the same engine as the web service, and give payment-status writes a single owner (either the worker or the app) with a migration that lives in db/migrations. - MediumAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in one of your files, so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth fixing when you next touch this area.
See technical detail
Issue class: a structural observation about how the system is designed and assembled. Rule identifier: architecture.security.auth-bypass-env-flag.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The environment inventory includes ADMIN_BYPASS_TOKEN alongside JWT_SIGNING_SECRET, yet no code in the scanned files reads either one and no authentication middleware exists. A named bypass token is a standing backdoor pattern: if any deployed code path honours it, it sidesteps whatever authorization is eventually added. Suggested remediation: Confirm whether anything consumes ADMIN_BYPASS_TOKEN; if so, remove that code path and rely on the normal token [secret redacted] flow, and drop the variable from all environments.
- MediumAI review
A structural point about how your app is put together
- What we found
- Looking at the shape of your project as a whole — how the pieces fit together and talk to each other — we noted something worth thinking about in badapp/.github/workflows/ci.yml (line 1).
- What it means
- This is about design rather than a specific bug: choices like this one tend to decide how easy the app is to change, and how well it holds up as it grows.
- What to do
- Read the detail below with whoever knows the codebase best and decide together whether it's worth adjusting now or noting for later — it's a trade-off, not an emergency. This is worth fixing when you next touch this area.
See technical detail
badapp/.github/workflows/ci.yml:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/.github/workflows/ci.yml:1. Rule identifier: architecture.maintainability.unpinned-ci-and-nondeterministic-install.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The workflow pins actions/checkout to a major tag and runs npm install, which resolves dependencies afresh and can mutate the lockfile, so CI is not reproducing the versions a deploy would use. The workflow also has no Node version setup step, so the runner's default Node decides the build, which may differ from the node:14 image in the Dockerfile. Reviewed: badapp/.github/workflows/ci.yml (lines 1-9) — quoted from the scanned source name: ci on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: npm install && npm test Suggested remediation: Add actions/setup-node pinned to the same Node major as the image, switch to npm ci, and pin actions to a full commit SHA. - MediumAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/docker-compose.yml (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth fixing when you next touch this area.
See technical detail
badapp/docker-compose.yml:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/docker-compose.yml:1. Rule identifier: architecture.security.database-port-published-to-host.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The db service maps 3306:3306, which makes the database reachable from outside the compose network rather than only from web and worker. The root password is supplied inline in the compose environment block, so it lands in shell history, process listings and docker inspect output. Reviewed: badapp/docker-compose.yml (lines 1-17) — quoted from the scanned source services: web: build: . ports: - "3000:3000" environment: - DATABASE_URL=mysql://root:[secret redacted]@db:3306/badapp db: image: mysql:5.7 ports: - "3306:3306" environment: - MYSQL_ROOT_PASSWORD=[secret redacted] worker: build: . command: python worker/db.py Suggested remediation: Remove the db ports mapping (service-to-service traffic already works over the compose network) and move MYSQL_ROOT_PASSWORD and DATABASE_URL into a secrets mechanism or an untracked env file. - MediumAI review
Visitor input reaches a system command
Effort: About an hour- What we found
- In badapp/Dockerfile (line 1), input from outside your app is used to build a command that the server then runs.
- What it means
- If someone shapes that input carefully, they may be able to run commands of their own choosing on your server.
- What to do
- Call the underlying program with a fixed list of arguments instead of building one long command string, and allow only values you have checked against a known-good list. This is worth fixing when you next touch this area.
See technical detail
badapp/Dockerfile:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/Dockerfile:1. Rule identifier: architecture.security.container-runs-as-root-with-full-context.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The Dockerfile does COPY . . with no .dockerignore evidence, then runs CMD as the default root user. That bakes local files — including any .env, git metadata and infrastructure code — into the image layers and gives a process that already has command-injection and eval sinks full root capability inside the container. Reviewed: badapp/Dockerfile (lines 1-8) — quoted from the scanned source FROM node:14 WORKDIR /app COPY . . RUN npm install # Runs as root, ships the whole tree including .env — infra findings. EXPOSE 3000 CMD ["node", "src/app.js"] Suggested remediation: Add a .dockerignore covering .env, .git, infra and node_modules, install with a production-only lockfile install, and add a non-root USER before CMD.
- MediumAI review
Visitor input is trusted directly in a database query
Effort: About an hour- What we found
- In badapp/src/db/queries.js (line 1), text that comes from whoever is using your app is dropped straight into a database instruction instead of being handled safely first.
- What it means
- A determined visitor could craft input that reads or changes data they should never be able to reach.
- What to do
- Use the database's built-in placeholders so anything a visitor types is always treated as data, never as an instruction. This is worth fixing when you next touch this area.
See technical detail
badapp/src/db/queries.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/db/queries.js:1. Rule identifier: architecture.maintainability.schema-drift-missing-table.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: The data layer exposes a query against reviews, but the migration set defines only users, products, orders and payments. Any caller of findReviews will fail at runtime with an unknown-table error, and the mismatch shows migrations are not the authoritative source for the schema the code assumes. Reviewed: badapp/src/db/queries.js (lines 1-13) — quoted from the scanned source const { connect } = require("./client"); // SQL injection: string concatenation straight into the query. exports.findUser = (id, cb) => connect().query("SELECT * FROM users WHERE id = '" + id + "'", cb); exports.findOrders = (userId, cb) => connect().query("SELECT * FROM orders WHERE user_id = '" + userId + "'", cb); // ARCHITECTURE SMELL: queries a `reviews` table that no migration creates (schema drift). exports.findReviews = (productId, cb) => connect().query("SELECT * FROM reviews WHERE product_id = '" + productId + "'", cb); Suggested remediation: Either add a migration creating reviews or delete findReviews, and add a CI step that applies migrations to a scratch database and exercises the query layer against it. - MediumAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/src/routes/admin.js (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. This is worth fixing when you next touch this area.
See technical detail
badapp/src/routes/admin.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/routes/admin.js:1. Rule identifier: architecture.maintainability.hardcoded-business-values-in-handler.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: POST /refund passes a literal card object into paymentService.charge, and charge itself inserts amount_cents as the constant 100 regardless of the order. The endpoint is also named refund while performing an insert into payments. Financial logic embedded as literals in a route handler is impossible to test or configure and will silently produce wrong ledger rows. Reviewed: badapp/src/routes/admin.js (lines 1-19) — quoted from the scanned source const express = require("express"); const { exec } = require("child_process"); const paymentService = require("../services/paymentService"); const router = express.Router(); // NO AUTH ON ANY OF THESE. The architecture finding is the missing boundary, not the syntax. // shell command built from user input -> command injection router.get("/ping", (req, res) => exec("ping -c 1 " + req.query.host, (err, stdout) => res.send(stdout)), ); router.post("/refund", (req, res) => paymentService.charge(req.query.orderId, { number: "[secret redacted]", cvv: "123", expiry: "12/30" }, (e) => res.json({ ok: !e })), ); module.exports = router; Suggested remediation: Derive the amount from the order record, pass card or token data in as a parameter rather than a literal, and rename the operation to match what it actually does. - MediumAI review
A structural point about how your app is put together
- What we found
- Looking at the shape of your project as a whole — how the pieces fit together and talk to each other — we noted something worth thinking about in badapp/src/routes/users.js (line 1).
- What it means
- This is about design rather than a specific bug: choices like this one tend to decide how easy the app is to change, and how well it holds up as it grows.
- What to do
- Read the detail below with whoever knows the codebase best and decide together whether it's worth adjusting now or noting for later — it's a trade-off, not an emergency. This is worth fixing when you next touch this area.
See technical detail
badapp/src/routes/users.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/routes/users.js:1. Rule identifier: architecture.maintainability.errors-discarded-in-callbacks.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: Handlers such as /user and /orders bind the error as e and never inspect it, sending res.json(rows) with rows undefined on failure, which yields a 200 with an empty body. /refund collapses every failure mode into ok: false with no detail or logging. Debugging a production incident with this pattern requires reproducing it locally. Reviewed: badapp/src/routes/users.js (lines 1-16) — quoted from the scanned source const express = require("express"); const queries = require("../db/queries"); const orderService = require("../services/orderService"); const router = express.Router(); router.get("/user", (req, res) => queries.findUser(req.query.id, (e, rows) => res.json(rows))); router.get("/orders", (req, res) => orderService.ordersFor(req.query.userId, (e, rows) => res.json(rows)), ); // eval on user input -> code injection router.get("/calc", (req, res) => res.send(String(eval(req.query.expr)))); module.exports = router; Suggested remediation: Add a shared error-handling path: check err in each callback, log it with context, and respond with an appropriate status code via Express's next(err) and a single error middleware. - MediumAI review
A structural point about how your app is put together
- What we found
- Looking at the shape of your project as a whole — how the pieces fit together and talk to each other — we noted something worth thinking about in badapp/src/services/paymentService.js (line 1).
- What it means
- This is about design rather than a specific bug: choices like this one tend to decide how easy the app is to change, and how well it holds up as it grows.
- What to do
- Read the detail below with whoever knows the codebase best and decide together whether it's worth adjusting now or noting for later — it's a trade-off, not an emergency. This is worth fixing when you next touch this area.
See technical detail
badapp/src/services/paymentService.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/services/paymentService.js:1. Rule identifier: architecture.maintainability.layering-violation-service-bypasses-data-layer.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: There is a db/queries module that owns SQL, but paymentService imports connect from db/client and issues its own INSERT. With two places allowed to write SQL, cross-cutting fixes — parameterisation, pooling, redaction of card fields in logs — have to be applied in more than one location and will drift. Reviewed: badapp/src/services/paymentService.js (lines 1-11) — quoted from the scanned source const { connect } = require("../db/client"); // ARCHITECTURE SMELL: a service reaching past the query layer into the driver directly, // AND persisting full card data (see 0002_add_payments.sql). exports.charge = (orderId, card, cb) => connect().query( "INSERT INTO payments (order_id, amount_cents, card_number, card_cvv, card_expiry) VALUES ('" + orderId + "', 100, '" + card.number + "', '" + card.cvv + "', '" + card.expiry + "')", cb, ); Suggested remediation: Move the payment insert into db/queries as a named, parameterised function and have paymentService depend only on that module. - LowAI review
Visitor input is trusted directly in a database query
Effort: About an hour- What we found
- In one of your files, text that comes from whoever is using your app is dropped straight into a database instruction instead of being handled safely first.
- What it means
- A determined visitor could craft input that reads or changes data they should never be able to reach.
- What to do
- Use the database's built-in placeholders so anything a visitor types is always treated as data, never as an instruction. There's no rush on this one.
See technical detail
Issue class: a structural observation about how the system is designed and assembled. Rule identifier: architecture.maintainability.declared-dependencies-unused.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: package.json declares jsonwebtoken, handlebars, lodash and minimist, and the Python side declares sqlalchemy, jinja2, pyyaml and requests, yet none of the scanned source imports them — the worker uses raw sqlite3 and the routes build HTML by concatenation. Unused runtime dependencies enlarge the image and the attack surface, and the presence of jsonwebtoken suggests an auth design that was never implemented. Suggested remediation: Remove dependencies that nothing imports, and if JWT auth is the intended design, implement it rather than leaving the library declared.
- LowAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/src/db/queries.js (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. There's no rush on this one.
See technical detail
badapp/src/db/queries.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/db/queries.js:1. Rule identifier: architecture.performance.select-star-queries.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: findUser, findOrders and findReviews all use SELECT *. Beyond leaking password_hash to the /user response, this couples the API payload to the physical schema, so adding a column silently changes what the endpoint returns and what crosses the wire. Reviewed: badapp/src/db/queries.js (lines 1-13) — quoted from the scanned source const { connect } = require("./client"); // SQL injection: string concatenation straight into the query. exports.findUser = (id, cb) => connect().query("SELECT * FROM users WHERE id = '" + id + "'", cb); exports.findOrders = (userId, cb) => connect().query("SELECT * FROM orders WHERE user_id = '" + userId + "'", cb); // ARCHITECTURE SMELL: queries a `reviews` table that no migration creates (schema drift). exports.findReviews = (productId, cb) => connect().query("SELECT * FROM reviews WHERE product_id = '" + productId + "'", cb); Suggested remediation: Enumerate the required columns in each query and map them to an explicit response shape in the route layer. - LowAI review
A password or key is written directly into the code
Effort: About an hour- What we found
- A password or key — the sort of secret value that unlocks a service — is written straight into your code in badapp/src/routes/admin.js (line 1), so it travels with every copy of the project.
- What it means
- Anyone who ends up with a copy of the code also gets the credential: a contractor, a future teammate, or anyone who finds the code somewhere it wasn't meant to be. It can be used in your name.
- What to do
- Move the value out of the code into a protected setting your app reads at start-up, then replace the old value at the service that issued it so the exposed one stops working. There's no rush on this one.
See technical detail
badapp/src/routes/admin.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/routes/admin.js:1. Rule identifier: architecture.maintainability.missing-body-parser-for-post-route.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: app.js registers no express.json() or urlencoded middleware, and POST /refund reads orderId from req.query. Request bodies are therefore unavailable to any handler, and mutating operations carry their parameters in the URL where they end up in access logs and browser history. Reviewed: badapp/src/routes/admin.js (lines 1-19) — quoted from the scanned source const express = require("express"); const { exec } = require("child_process"); const paymentService = require("../services/paymentService"); const router = express.Router(); // NO AUTH ON ANY OF THESE. The architecture finding is the missing boundary, not the syntax. // shell command built from user input -> command injection router.get("/ping", (req, res) => exec("ping -c 1 " + req.query.host, (err, stdout) => res.send(stdout)), ); router.post("/refund", (req, res) => paymentService.charge(req.query.orderId, { number: "[secret redacted]", cvv: "123", expiry: "12/30" }, (e) => res.json({ ok: !e })), ); module.exports = router; Suggested remediation: Register express.json() in app.js and change /refund to read its parameters from the request body. - LowAI review
A structural point about how your app is put together
- What we found
- Looking at the shape of your project as a whole — how the pieces fit together and talk to each other — we noted something worth thinking about in badapp/src/services/orderService.js (line 1).
- What it means
- This is about design rather than a specific bug: choices like this one tend to decide how easy the app is to change, and how well it holds up as it grows.
- What to do
- Read the detail below with whoever knows the codebase best and decide together whether it's worth adjusting now or noting for later — it's a trade-off, not an emergency. There's no rush on this one.
See technical detail
badapp/src/services/orderService.js:1
Issue class: a structural observation about how the system is designed and assembled. Location: badapp/src/services/orderService.js:1. Rule identifier: architecture.maintainability.unstructured-logging.
Show the original technical report
Shown exactly as our checks reported it, with two exceptions: credential values are masked, and internal system names are removed.
Reported: lib/logger.js emits a timestamp and a concatenated string, and orderService interpolates the caller-supplied userId into it. There is no level, no request correlation id and no structure, so logs cannot be filtered or joined to a request, and newline characters in the input can forge log lines. Reviewed: badapp/src/services/orderService.js (lines 1-8) — quoted from the scanned source const queries = require("../db/queries"); const { log } = require("../lib/logger"); exports.ordersFor = (userId, cb) => { log("loading orders for " + userId); return queries.findOrders(userId, cb); }; Suggested remediation: Adopt a structured logger with levels and per-request context, and pass user-supplied values as separate fields rather than concatenating them into the message.
Architecture
Back to topA deeper look at how your project is put together — its structure, and an AI-assisted read on the design choices behind it.
Architecture identification
Small Express monolith with a separate Python worker container and SQL migrations
High confidence- Node.js 14 (Docker base image node:14)
- Express 4.16.0
- mysql (node driver) 2.16.0
- jsonwebtoken 8.1.0
- lodash 4.17.4, handlebars 4.0.11, minimist 1.2.0
- Python worker: sqlite3 + subprocess (stdlib)
- SQLAlchemy 1.2.0, requests 2.19.1, jinja2 2.10, pyyaml 3.13 (declared in worker/requirements.txt)
- MySQL 5.7 (docker-compose db service)
- Raw SQL migration files (db/migrations)
- Terraform (AWS S3 bucket, security group)
- GitHub Actions CI (ubuntu-latest)
- nodemon 1.18.4 (dev)
Layers
- HTTP routing
- Express routers in src/routes split into public, users and admin; each handler parses query params and writes the response directly. app.js mounts all three, with the admin router mounted at /admin without any authentication middleware.
- Service layer
- src/services holds orderService (delegates to the query module) and paymentService. The layering is inconsistent: paymentService bypasses the query module and builds an INSERT against the driver connection itself.
- Data access
- src/db/client.js creates a new MySQL connection per call using credentials imported from a checked-in config module; src/db/queries.js builds SELECTs by string concatenation and queries a 'reviews' table that no migration defines.
- Schema / migrations
- db/migrations contains 0001_init.sql (users, products, orders) and 0002_add_payments.sql (payments, including card_number, card_cvv and card_expiry columns in plaintext, plus an orders.paid_at column).
- Background worker
- worker/db.py runs as its own compose service and reads DATABASE_URL, but connects to a local sqlite file app.db rather than the MySQL instance, and updates orders independently of the Node schema owner.
- Configuration
- src/config.js exports literal credential values (AWS keys, API token, admin password, JWT secret) as a module rather than reading environment variables.
- Infrastructure
- Dockerfile (node:14, copies the whole tree, exposes 3000), docker-compose.yml (web, db, worker), infra/main.tf (S3 backups bucket with public-read ACL, security group allowing TCP 0-65535 from 0.0.0.0/0), and a GitHub Actions push workflow.
Entry points
- src/app.js — Process entry point; builds the Express app, mounts routers and listens on port 3000.
- Public routes (src/routes/public.js) — GET /health returns a JSON status; GET /hello interpolates req.query.name straight into an HTML response.
- User routes (src/routes/users.js) — GET /user and GET /orders read query params and pass them to the data layer; GET /calc passes req.query.expr to eval.
- Admin routes (src/routes/admin.js, mounted at /admin) — GET /admin/ping passes req.query.host into child_process.exec; POST /admin/refund calls paymentService.charge. Neither has an auth check in the code shown.
- Worker container command — docker-compose runs 'python worker/db.py' as the worker service; the file defines lookup, mark_paid, backup and run functions with no visible scheduler or queue consumer.
- npm scripts — package.json declares 'start' and 'migrate' scripts.
- GitHub Actions workflow — .github/workflows/ci.yml triggers on push and runs npm install && npm test, though no test files were found in the scan.
External integrations
- MySQL 5.7 — Primary datastore for the web service, reached via the mysql Node driver and the DATABASE_URL in docker-compose.
- SQLite (app.db) — Second, independent datastore used by the Python worker for user lookups and order status updates.
- AWS S3 — Terraform declares a 'badapp-backups' bucket for backups; the worker also produces a local backup.tgz via tar.
- AWS (IAM credentials) — AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env names plus literal key values in src/config.js suggest SDK or CLI access to AWS resources.
- Stripe — STRIPE_SECRET_KEY appears in the environment variable inventory; no client code for it is present in the scanned files, while paymentService writes card data to the local database instead.
- GitHub — GITHUB_TOKEN env name and the Actions CI workflow using actions/checkout@v2.
- JWT signing — jsonwebtoken dependency and JWT_SIGNING_SECRET / ADMIN_BYPASS_TOKEN env names imply token-based auth, but no verification middleware appears in the routes shown.
- HTTP client (requests) — Declared in the worker's requirements, implying outbound HTTP calls, though no call site appears in the scanned worker code.
Architecture diagram
Select a box above to see its details and connections here.
View the full diagram as a list
- badapp — Module
23 files, 275 lines
The single application module (23 files, 275 lines) holding the Express HTTP layer, the SQL migrations and a Python worker subdirectory; effectively the whole codebase.
- Connects to badapp endpoints (exposes)
- Connects to Data model (declares schema)
- Connects to Express (declared in badapp/package.json)
- Connects to JWT (declared in badapp/package.json)
- Connects to mysql (declared in badapp/package.json)
- Connects to handlebars (declared in badapp/package.json)
- Connects to lodash (declared in badapp/package.json)
- Connects to minimist (declared in badapp/package.json)
- aws_s3_bucket.backups — Service
declares 1 port
Terraform-declared S3 bucket in badapp/infra/main.tf, apparently for backups.
- aws_security_group.web — Service
declares 1 port
Terraform security group for the web tier; the scan recorded port 65535, which usually indicates a rule spanning the entire port range and is worth reviewing.
- badapp/Dockerfile — Service
image node:14, declares 1 port
Container build for the Node service, based on node:14 and exposing port 3000; that base tag is well past its maintenance window.
- Connects to badapp (builds the application module into the node:14 image)
- build — Service
image ubuntu-latest
CI job in badapp/.github/workflows/ci.yml running on ubuntu-latest.
- db — Service
image mysql:5.7, declares 2 ports
MySQL 5.7 database backing the application, with 3306 surfaced in the compose port list alongside the app's 3000.
- Connects to Data model (inferred: hosts the tables created by the SQL migrations)
- steps — Service
image ubuntu-latest
Second entry parsed from the CI workflow on ubuntu-latest; likely a steps block read as a job name rather than a real second job.
- web — Service
image mysql:5.7, declares 2 ports
Compose service for the HTTP app; the scan attributed the compose file's mysql:5.7 image and its 3000/3306 port pair to every service in the file, so the per-service image here is unreliable.
- Connects to db (inferred: app uses the mysql driver against the compose MySQL service)
- worker — Service
image mysql:5.7, declares 2 ports
Background worker declared in compose; its Python dependencies (SQLAlchemy 1.2.0, requests, pyyaml, jinja2) come from badapp/worker/requirements.txt.
- Connects to SQLAlchemy (badapp/worker/requirements.txt)
- Connects to requests (badapp/worker/requirements.txt)
- Connects to pyyaml (badapp/worker/requirements.txt)
- Connects to jinja2 (badapp/worker/requirements.txt)
- badapp endpoints — Routes
7 routes (GET, POST)
Seven GET/POST HTTP endpoints exposed by the Express app; no test files were found anywhere in the project to cover them.
- Data model — Data store
4 tables, declared via sql-migrations
Schema declared through raw SQL migrations (no dialect recorded) with four tables: users, products, orders and payments.
- Express — External
web, 4.16.0
Web framework for the HTTP layer, pinned at 4.16.0 in badapp/package.json.
- handlebars — External
dependency
Templating library on the Node side, presumably rendering responses for the Express routes.
- jinja2 — External
dependency
Templating library declared for the Python worker.
- JWT — External
auth, declared as jsonwebtoken, 8.1.0
Token-based authentication library (jsonwebtoken 8.1.0); verify that verification options and algorithms are set explicitly wherever it is used.
- lodash — External
dependency
General-purpose JavaScript utility library used by the Node app.
- minimist — External
dependency
Command-line argument parser, suggesting a Node CLI or script entry point alongside the web server.
- mysql — External
dependency
Node MySQL driver, the app's path to the db service; with raw SQL migrations in play, check that queries are parameterised.
- pyyaml — External
dependency
YAML parser in the worker; confirm it uses safe loading if it ever reads externally supplied documents.
- requests — External
dependency
HTTP client for the Python worker's outbound calls.
- SQLAlchemy — External
orm, ==1.2.0
ORM used by the Python worker, pinned to ==1.2.0 in badapp/worker/requirements.txt.
How your architecture holds up
The overall picture
The shape of this system — an Express web app, a separate Python worker container, SQL migration files, Docker Compose for local wiring and Terraform for cloud resources — is a sensible skeleton for a small commerce-style service. But the implementation contradicts its own structure in ways that matter: the worker talks to a SQLite file while the web app talks to MySQL, credentials are literal values in src/config.js rather than environment variables, and the admin router is mounted with no authentication despite jsonwebtoken being a dependency. The one takeaway: this reads as an unsafe demo or test fixture, not something to put in front of real users or real card data.
What genuinely works is the separation of concerns on paper. Routes are split into public, users and admin; there is a service layer; schema changes live in versioned files (0001_init.sql, 0002_add_payments.sql); slow work is pushed to a worker process so HTTP stays responsive. At 291 lines across 24 files, all of this is still cheap to fix — that is the real asset here.
The structural strains are concentrated in three places. First, data ownership: worker/db.py updates orders in a local app.db while the Node service owns the MySQL orders table, so the two will silently diverge; DATABASE_URL is read but not used to reach MySQL. Second, the layering is not enforced — paymentService goes straight to the driver rather than through src/db/queries.js, and queries.js selects from a 'reviews' table that no migration creates. Third, trust boundaries are absent by design: query params flow into eval (GET /calc), child_process.exec (GET /admin/ping) and concatenated SQL, and the payments migration stores card_number, card_cvv and card_expiry in plaintext.
Read the full assessment
Infrastructure repeats the pattern. infra/main.tf declares a public-read backups bucket and a security group opening TCP 0-65535 to 0.0.0.0/0, and CI runs npm test while the scan found no test files — so the pipeline is green by default rather than by evidence.
What’s working
- The service boundaries are drawn along sensible lines. Routing lives in src/routes (public, users, admin), business logic in src/services, data access in src/db, and slow work in a separate worker container declared in docker-compose.yml — the right skeleton for a small app that needs to grow.
- Schema changes are versioned as plain SQL rather than being applied by hand. db/migrations/0001_init.sql and 0002_add_payments.sql define users, products, orders and payments, and package.json exposes a 'migrate' script, so database state is reproducible.
- The whole environment is reproducible locally. A Dockerfile (node:14, port 3000) plus docker-compose.yml wiring web, db (mysql:5.7) and worker means a new developer gets the same stack without manual setup.
- Infrastructure is described in code rather than clicked into a console. infra/main.tf declares the S3 backups bucket and web security group, so cloud state is reviewable in the same pull request as the code that depends on it.
- A CI pipeline already exists and fires on push. .github/workflows/ci.yml on ubuntu-latest with actions/checkout@v2 is the hook that test and lint gates can attach to later, at near-zero cost.
- The system is small enough to correct decisively. 24 files and 291 lines across one module means the data-ownership and configuration problems can be fixed in days rather than being carried as long-term debt.
Worth reconsidering
- Two databases claim to own the same data. The web service writes orders to MySQL via src/db/client.js, while worker/db.py reads DATABASE_URL but connects to a local sqlite app.db and marks orders paid there. These will diverge silently, and order state becomes unanswerable — this is the single most damaging structural flaw.
- Secrets are a code module rather than configuration. src/config.js exports literal AWS keys, an API token, an admin password and a JWT secret, so every clone of the repository is a credential leak and rotating a key requires a code change and redeploy.
- The admin surface has no gate in front of it. app.js mounts the admin router at /admin, and /admin/ping and POST /admin/refund carry no auth check, despite jsonwebtoken 8.1.0 being a dependency and JWT_SIGNING_SECRET and ADMIN_BYPASS_TOKEN appearing in the environment inventory. Refunds are reachable by anyone who can reach the port.
- Untrusted input reaches interpreters by design, not by accident. GET /calc passes req.query.expr to eval, GET /admin/ping passes req.query.host to child_process.exec, and GET /hello interpolates req.query.name into HTML. Each is remote code execution or script injection with no input boundary anywhere in the route layer.
- SQL is assembled by string concatenation. src/db/queries.js builds SELECTs by joining strings rather than using the mysql driver's parameter binding, so any route that forwards a query param to the data layer is an injection path.
- Cardholder data is stored in your own database in the clear. 0002_add_payments.sql defines card_number, card_cvv and card_expiry columns, and paymentService writes to them — even though STRIPE_SECRET_KEY is in the environment, implying a processor was meant to hold this instead. This is a compliance problem as much as a technical one.
- The service layer is not actually a boundary. orderService delegates to the query module but paymentService builds its own INSERT against the driver connection, so there is no single place to add validation, auditing or transactions.
- The data access layer queries a table the schema does not define. src/db/queries.js reads from 'reviews', which appears in no migration — either dead code or a runtime failure waiting on a live database.
- Cloud resources are declared wide open. infra/main.tf sets the badapp-backups bucket to public-read and a security group allowing TCP 0-65535 from 0.0.0.0/0, so backups and every container port are internet-reachable by default.
- CI reports success without verifying anything. The workflow runs npm install && npm test, but the scan found zero test files, so the green check is decorative and offers no protection against regressions.
- A new MySQL connection is opened per call in src/db/client.js. With no pooling, the service will exhaust connection limits under modest concurrency, and there is no place to enforce transaction scope across a multi-step operation like a refund.
- The runtime base is long past end of life. The Dockerfile builds on node:14 and the app pins Express 4.16.0 and mysql 2.16.0, so security patches for the platform itself are no longer arriving.
- The worker has no trigger mechanism. worker/db.py defines lookup, mark_paid, backup and run, but docker-compose just runs the script with no scheduler or queue consumer visible, so it is unclear what causes work to happen or what happens if it fails midway.
- The worker declares dependencies it does not appear to use. sqlalchemy, requests, jinja2 and pyyaml are pinned in worker/requirements.txt while the scanned code uses stdlib sqlite3 and subprocess — either unfinished integration or unnecessary attack surface.
- docker-compose.yml publishes 3306 alongside 3000. Exposing the database port on the host is convenient locally but is a habit that carries badly into any shared or deployed environment.
Data
Back to topYour project's data model — its tables, columns, and how they relate to each other — mapped directly from its own schema and code.
Database diagram
Select a box above to see its details and connections here.
View the full diagram as a list
- users — Table
badapp/db/migrations/0001_init.sql
- id — uuid (PK)
- email — text
- password_hash — text
- is_admin — boolean
- created_at — timestamptz
- products — Table
badapp/db/migrations/0001_init.sql
- id — uuid (PK)
- sku — varchar(64)
- name — text
- price_cents — integer
- orders — Table
badapp/db/migrations/0001_init.sql
- id — uuid (PK)
- user_id — uuid (FK)
- product_id — uuid (FK)
- quantity — integer
- status — text
- paid_at — timestamptz · optional
- Connects to users (user_id → id)
- Connects to products (product_id → id)
- payments — Table
badapp/db/migrations/0002_add_payments.sql
- id — uuid (PK)
- order_id — uuid (FK)
- amount_cents — integer
- currency — varchar(3)
- card_number — text
- card_cvv — text
- card_expiry — text
- Connects to orders (order_id → id)
Data model dictionary
- 4 tables
- 22 columns
- 3 relationships
orders6 columns · 2 foreign keys
| Column | Type | Nullable | Key | Default |
|---|---|---|---|---|
| id | uuid | No | PK | gen_random_uuid() |
| user_id | uuid | No | FK → users.id | — |
| product_id | uuid | No | FK → products.id | — |
| quantity | integer | No | — | 1 |
| status | text | No | — | 'pending' |
| paid_at | timestamptz | Yes | — | — |
Indexes: orders_user_idx
badapp/db/migrations/0001_init.sql
payments7 columns · 1 foreign key
| Column | Type | Nullable | Key | Default |
|---|---|---|---|---|
| id | uuid | No | PK | gen_random_uuid() |
| order_id | uuid | No | FK → orders.id | — |
| amount_cents | integer | No | — | — |
| currency | varchar(3) | No | — | 'usd' |
| card_number | text | No | — | — |
| card_cvv | text | No | — | — |
| card_expiry | text | No | — | — |
Indexes: payments_order_idx
badapp/db/migrations/0002_add_payments.sql
products4 columns
| Column | Type | Nullable | Key | Default |
|---|---|---|---|---|
| id | uuid | No | PK | gen_random_uuid() |
| sku | varchar(64) | No | Unique | — |
| name | text | No | — | — |
| price_cents | integer | No | — | — |
Checks: "price_cents" > 0
badapp/db/migrations/0001_init.sql
users5 columns
| Column | Type | Nullable | Key | Default |
|---|---|---|---|---|
| id | uuid | No | PK | gen_random_uuid() |
| text | No | Unique | — | |
| password_hash | text | No | — | — |
| is_admin | boolean | No | — | false |
| created_at | timestamptz | No | — | now() |
badapp/db/migrations/0001_init.sql
A note on these results
This report reflects what the analysis could find — it’s expert guidance to act on, not a guarantee that your code is free of every issue.
Ready to see yours?
Create an account, tell us a little about what you’ve built, and pick the scan that fits. Your code is isolated during analysis and automatically deleted afterward.
Start your analysis