Here's a login endpoint from a small Express demo app
( scan-target-demo-apps/apps/01-sql-injection):
app.post('/login', (req, res) => {
const { username = '', password = '' } = req.body;
const query = `SELECT id, username, email, is_admin FROM users
WHERE username = '${username}' AND password = '${password}'`;
const result = db.exec(query);
// ...
});
You've seen this shape before. username
and password
go straight into the SQL string — no
parameter binding, no escaping. Submit admin' --
as the username and anything as the password,
and the query becomes:
SELECT id, username, email, is_admin FROM users
WHERE username = 'admin' -- ' AND password = 'anything'
--
comments out the rest of the line. The password check never runs. {"ok": true}
, logged in
as admin
.
Classic CWE-89, OWASP A03:2021. Nothing novel here — the interesting part is what caught it.
We ran this target through AI Security Studio, an offline security research platform we've
been building. No cloud calls, no code leaves the machine — everything you're about to read
happened on a laptop with no network access to anywhere but localhost:3001
.
The important design decision: the LLM never sees raw source code or raw HTTP traffic.
The pipeline looks like this:
Parser → Rule Engine → Knowledge Retrieval → Summarization → LLM → Reasoning → Finding → Report
A deterministic parser and rule engine do the actual finding. The local LLM only explains
already-discovered evidence — it's not asked to eyeball a codebase and guess where the bugs
might be. If the evidence isn't there, the platform is built to say "Needs Manual Verification"
instead of inventing a conclusion.
For this run, that meant:
http://localhost:3001
as scope.server.js:56
and the missing
auth check on the state-changing POST /login
route.The generated report includes the actual vulnerable line as evidence, not a generic "possible SQL
injection" label:
Source preview.
app.post('/login', (req, res) => {
const { username = '', password = '' } = req.body;
const query = `SELECT id, username, email, is_admin FROM users WHERE username = '${username}' AND password = '${password}'`;
Risk: High. Root cause: CWE-89 (and a separate CWE-306 finding for the missing authorization
check). Suggested fix: parameterized queries. Confidence explicitly marked — some findings from
this pass are "Confirmed," others are flagged "Needs Manual Verification" rather than asserted,
because header/config-level evidence alone doesn't prove exploitability the way a manual
Active Test / Repeater run does.
It's easy to gloss over "runs locally" as a checkbox feature. In practice it means:
The demo app is intentionally vulnerable and open source — safe to point any scanner at, including
this one:
Built for security researchers, pentesters, and AppSec teams doing work they're authorized to do.
Next up in this series: stored XSS — one comment field, every visitor who loads the page.