[pitboss] phase 13: Track L.11 — Express / Koa / NestJS / Fastify adapters

This commit is contained in:
pitboss 2026-05-18 12:14:53 -05:00
parent 9ed837be9b
commit 04bf7b997f
27 changed files with 2670 additions and 11 deletions

View file

@ -0,0 +1,28 @@
// Phase 13 (Track L.11) — Express CMDI benign fixture.
//
// The `/run` route accepts a `cmd` query parameter but rejects
// everything outside an allowlist before invoking `child_process.exec`
// with a fixed argv, so the sink call is unreachable for
// attacker-controlled values.
const express = require('express');
const { execFile } = require('child_process');
const app = express();
const ALLOW = new Set(['status', 'uptime', 'version']);
function runCmd(req, res) {
const cmd = req.query.cmd || '';
if (!ALLOW.has(cmd)) {
return res.status(400).send('rejected');
}
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
if (err) return res.status(500).send(String(err));
res.send(stdout);
});
}
app.get('/run', runCmd);
module.exports = { app, runCmd };

View file

@ -0,0 +1,23 @@
// Phase 13 (Track L.11) — Express CMDI vuln fixture.
//
// The `/run` route forwards a `cmd` query parameter straight into
// `child_process.exec`, so any attacker who reaches the route can
// execute arbitrary shell. Adapter binding:
// `app.get('/run', runCmd)` with `cmd` flowing through `req.query.cmd`.
const express = require('express');
const { exec } = require('child_process');
const app = express();
function runCmd(req, res) {
const cmd = req.query.cmd || '';
exec(cmd, (err, stdout) => {
if (err) return res.status(500).send(String(err));
res.send(stdout);
});
}
app.get('/run', runCmd);
module.exports = { app, runCmd };

View file

@ -0,0 +1,28 @@
// Phase 13 (Track L.11) — Fastify CMDI benign fixture.
//
// The `/run` route accepts a `cmd` query parameter but rejects
// everything outside an allowlist before invoking
// `child_process.execFile` with a fixed argv.
const fastify = require('fastify')();
const { execFile } = require('child_process');
const ALLOW = new Set(['status', 'uptime', 'version']);
async function runCmd(request, reply) {
const cmd = request.query.cmd || '';
if (!ALLOW.has(cmd)) {
reply.code(400).send('rejected');
return;
}
const out = await new Promise((resolve) => {
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
resolve(err ? String(err) : stdout);
});
});
reply.send(out);
}
fastify.get('/run', runCmd);
module.exports = { app: fastify, runCmd };

View file

@ -0,0 +1,20 @@
// Phase 13 (Track L.11) — Fastify CMDI vuln fixture.
//
// The `/run` route forwards a `cmd` query parameter straight into
// `child_process.exec`. Adapter binding: `fastify.get('/run', runCmd)`
// with `cmd` flowing through `request.query.cmd`.
const fastify = require('fastify')();
const { exec } = require('child_process');
async function runCmd(request, reply) {
const cmd = request.query.cmd || '';
const out = await new Promise((resolve) => {
exec(cmd, (err, stdout) => resolve(err ? String(err) : stdout));
});
reply.send(out);
}
fastify.get('/run', runCmd);
module.exports = { app: fastify, runCmd };

View file

@ -0,0 +1,34 @@
// Phase 13 (Track L.11) — Koa CMDI benign fixture.
//
// The `/run` route accepts a `cmd` query parameter but rejects
// everything outside an allowlist before invoking `child_process.execFile`
// with a fixed argv.
const Koa = require('koa');
const Router = require('@koa/router');
const { execFile } = require('child_process');
const app = new Koa();
const router = new Router();
const ALLOW = new Set(['status', 'uptime', 'version']);
async function runCmd(ctx) {
const cmd = ctx.query.cmd || '';
if (!ALLOW.has(cmd)) {
ctx.status = 400;
ctx.body = 'rejected';
return;
}
await new Promise((resolve) => {
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
ctx.body = err ? String(err) : stdout;
resolve();
});
});
}
router.get('/run', runCmd);
app.use(router.routes());
module.exports = { app, runCmd };

View file

@ -0,0 +1,27 @@
// Phase 13 (Track L.11) — Koa CMDI vuln fixture.
//
// The `/run` route forwards a `cmd` query parameter straight into
// `child_process.exec`. Adapter binding: `router.get('/run', runCmd)`
// with `cmd` flowing through `ctx.query.cmd`.
const Koa = require('koa');
const Router = require('@koa/router');
const { exec } = require('child_process');
const app = new Koa();
const router = new Router();
async function runCmd(ctx) {
const cmd = ctx.query.cmd || '';
await new Promise((resolve) => {
exec(cmd, (err, stdout) => {
ctx.body = err ? String(err) : stdout;
resolve();
});
});
}
router.get('/run', runCmd);
app.use(router.routes());
module.exports = { app, runCmd };

View file

@ -0,0 +1,26 @@
// Phase 13 (Track L.11) — NestJS CMDI benign fixture. Same adapter
// binding shape as the vuln fixture; the differential outcome is what
// distinguishes the two.
require('reflect-metadata');
const { Controller, Get, Query } = require('@nestjs/common');
const { execFile } = require('child_process');
const ALLOW = new Set(['status', 'uptime', 'version']);
@Controller('')
class AppController {
@Get('run')
runCmd(@Query('cmd') cmd) {
if (!ALLOW.has(cmd || '')) {
return 'rejected';
}
return new Promise((resolve) => {
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
resolve(err ? String(err) : stdout);
});
});
}
}
module.exports = { AppController };

View file

@ -0,0 +1,27 @@
// Phase 13 (Track L.11) — NestJS CMDI vuln fixture (Babel-stage-1
// decorator syntax form). Real Nest projects publish their
// controllers either as `.ts` files or as Babel-transpiled `.js`
// carrying the inline decorator syntax via `@babel/plugin-proposal-decorators`
// + `reflect-metadata`. The adapter binds the decorator syntax;
// the harness loads the entry via `Test.createTestingModule`.
//
// Adapter binding: `@Controller('')` + `@Get('run')` on
// `AppController.runCmd` with `cmd` flowing through `@Query('cmd')`.
require('reflect-metadata');
const { Controller, Get, Query } = require('@nestjs/common');
const { exec } = require('child_process');
@Controller('')
class AppController {
@Get('run')
runCmd(@Query('cmd') cmd) {
return new Promise((resolve) => {
exec(cmd || '', (err, stdout) => {
resolve(err ? String(err) : stdout);
});
});
}
}
module.exports = { AppController };

View file

@ -0,0 +1,27 @@
// Phase 13 (Track L.11) — Express CMDI benign fixture (TypeScript).
import express, { Request, Response } from 'express';
import { execFile } from 'child_process';
const app = express();
const ALLOW = new Set(['status', 'uptime', 'version']);
function runCmd(req: Request, res: Response) {
const cmd = (req.query.cmd as string) || '';
if (!ALLOW.has(cmd)) {
res.status(400).send('rejected');
return;
}
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
if (err) {
res.status(500).send(String(err));
return;
}
res.send(stdout);
});
}
app.get('/run', runCmd);
export { app, runCmd };

View file

@ -0,0 +1,23 @@
// Phase 13 (Track L.11) — Express CMDI vuln fixture (TypeScript).
// Same shape as the JS twin; binds `app.get('/run', runCmd)` and
// flows `req.query.cmd` straight into `exec`.
import express, { Request, Response } from 'express';
import { exec } from 'child_process';
const app = express();
function runCmd(req: Request, res: Response) {
const cmd = (req.query.cmd as string) || '';
exec(cmd, (err, stdout) => {
if (err) {
res.status(500).send(String(err));
return;
}
res.send(stdout);
});
}
app.get('/run', runCmd);
export { app, runCmd };

View file

@ -0,0 +1,25 @@
// Phase 13 (Track L.11) — Fastify CMDI benign fixture (TypeScript).
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
import { execFile } from 'child_process';
const app = Fastify();
const ALLOW = new Set(['status', 'uptime', 'version']);
async function runCmd(request: FastifyRequest, reply: FastifyReply): Promise<void> {
const cmd = ((request.query as Record<string, string>).cmd) || '';
if (!ALLOW.has(cmd)) {
reply.code(400).send('rejected');
return;
}
const out = await new Promise<string>((resolve) => {
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
resolve(err ? String(err) : stdout);
});
});
reply.send(out);
}
app.get('/run', runCmd);
export { app, runCmd };

View file

@ -0,0 +1,18 @@
// Phase 13 (Track L.11) — Fastify CMDI vuln fixture (TypeScript).
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
import { exec } from 'child_process';
const app = Fastify();
async function runCmd(request: FastifyRequest, reply: FastifyReply): Promise<void> {
const cmd = ((request.query as Record<string, string>).cmd) || '';
const out = await new Promise<string>((resolve) => {
exec(cmd, (err, stdout) => resolve(err ? String(err) : stdout));
});
reply.send(out);
}
app.get('/run', runCmd);
export { app, runCmd };

View file

@ -0,0 +1,29 @@
// Phase 13 (Track L.11) — Koa CMDI benign fixture (TypeScript).
import Koa from 'koa';
import Router from '@koa/router';
import { execFile } from 'child_process';
const app = new Koa();
const router = new Router();
const ALLOW = new Set(['status', 'uptime', 'version']);
async function runCmd(ctx: Koa.Context): Promise<void> {
const cmd = (ctx.query.cmd as string) || '';
if (!ALLOW.has(cmd)) {
ctx.status = 400;
ctx.body = 'rejected';
return;
}
await new Promise<void>((resolve) => {
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
ctx.body = err ? String(err) : stdout;
resolve();
});
});
}
router.get('/run', runCmd);
app.use(router.routes());
export { app, runCmd };

View file

@ -0,0 +1,23 @@
// Phase 13 (Track L.11) — Koa CMDI vuln fixture (TypeScript).
import Koa from 'koa';
import Router from '@koa/router';
import { exec } from 'child_process';
const app = new Koa();
const router = new Router();
async function runCmd(ctx: Koa.Context): Promise<void> {
const cmd = (ctx.query.cmd as string) || '';
await new Promise<void>((resolve) => {
exec(cmd, (err, stdout) => {
ctx.body = err ? String(err) : stdout;
resolve();
});
});
}
router.get('/run', runCmd);
app.use(router.routes());
export { app, runCmd };

View file

@ -0,0 +1,22 @@
// Phase 13 (Track L.11) — NestJS CMDI benign fixture (TypeScript).
import 'reflect-metadata';
import { Controller, Get, Query } from '@nestjs/common';
import { execFile } from 'child_process';
const ALLOW = new Set(['status', 'uptime', 'version']);
@Controller('')
export class AppController {
@Get('run')
runCmd(@Query('cmd') cmd: string): Promise<string> | string {
if (!ALLOW.has(cmd || '')) {
return 'rejected';
}
return new Promise((resolve) => {
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
resolve(err ? String(err) : stdout);
});
});
}
}

View file

@ -0,0 +1,20 @@
// Phase 13 (Track L.11) — NestJS CMDI vuln fixture (TypeScript).
//
// Adapter binding: `@Controller('')` + `@Get('run')` on
// `AppController.runCmd` with `cmd` flowing through `@Query('cmd')`.
import 'reflect-metadata';
import { Controller, Get, Query } from '@nestjs/common';
import { exec } from 'child_process';
@Controller('')
export class AppController {
@Get('run')
runCmd(@Query('cmd') cmd: string): Promise<string> {
return new Promise((resolve) => {
exec(cmd || '', (err, stdout) => {
resolve(err ? String(err) : stdout);
});
});
}
}

View file

@ -0,0 +1,182 @@
//! Phase 13 (Track L.11) — JS framework adapter integration tests.
//!
//! Each test exercises `detect_binding` end-to-end against a fixture
//! file under `tests/dynamic_fixtures/js_frameworks/`, asserting that
//! the right adapter fires, the binding carries
//! `EntryKind::HttpRoute`, and the `RouteShape` + per-formal
//! `request_params` match the brief's contract. Benign fixtures must
//! produce the same adapter binding shape as the vuln fixtures — the
//! adapter only models the route, the differential outcome of a
//! verifier run is what distinguishes the two.
#![cfg(feature = "dynamic")]
use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource};
use nyx_scanner::evidence::EntryKind;
use nyx_scanner::summary::FuncSummary;
use nyx_scanner::symbol::Lang;
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
fn summary_for(name: &str, file: &str) -> FuncSummary {
FuncSummary {
name: name.into(),
file_path: file.into(),
lang: "javascript".into(),
..Default::default()
}
}
#[test]
fn express_vuln_fixture_binds_route() {
let path = "tests/dynamic_fixtures/js_frameworks/express/vuln.js";
let bytes = std::fs::read(path).expect("express vuln fixture exists");
let tree = parse_js(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
.expect("express adapter must bind");
assert_eq!(binding.adapter, "js-express");
assert_eq!(binding.kind, EntryKind::HttpRoute);
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.method, HttpMethod::GET);
assert!(binding
.request_params
.iter()
.any(|p| p.name == "req" && matches!(p.source, ParamSource::Implicit)));
}
#[test]
fn express_benign_fixture_binds_same_route_shape() {
let path = "tests/dynamic_fixtures/js_frameworks/express/benign.js";
let bytes = std::fs::read(path).expect("express benign fixture exists");
let tree = parse_js(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
.expect("express adapter must bind benign fixture");
assert_eq!(binding.adapter, "js-express");
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.method, HttpMethod::GET);
}
#[test]
fn koa_vuln_fixture_binds_router_route() {
let path = "tests/dynamic_fixtures/js_frameworks/koa/vuln.js";
let bytes = std::fs::read(path).expect("koa vuln fixture exists");
let tree = parse_js(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
.expect("koa adapter must bind");
assert_eq!(binding.adapter, "js-koa");
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.method, HttpMethod::GET);
assert!(binding
.request_params
.iter()
.any(|p| p.name == "ctx" && matches!(p.source, ParamSource::Implicit)));
}
#[test]
fn koa_benign_fixture_binds_same_route_shape() {
let path = "tests/dynamic_fixtures/js_frameworks/koa/benign.js";
let bytes = std::fs::read(path).expect("koa benign fixture exists");
let tree = parse_js(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
.expect("koa adapter must bind benign fixture");
assert_eq!(binding.adapter, "js-koa");
assert_eq!(binding.route.as_ref().unwrap().path, "/run");
}
#[test]
fn fastify_vuln_fixture_binds_route() {
let path = "tests/dynamic_fixtures/js_frameworks/fastify/vuln.js";
let bytes = std::fs::read(path).expect("fastify vuln fixture exists");
let tree = parse_js(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
.expect("fastify adapter must bind");
assert_eq!(binding.adapter, "js-fastify");
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.method, HttpMethod::GET);
assert!(binding
.request_params
.iter()
.any(|p| p.name == "request" && matches!(p.source, ParamSource::Implicit)));
assert!(binding
.request_params
.iter()
.any(|p| p.name == "reply" && matches!(p.source, ParamSource::Implicit)));
}
#[test]
fn fastify_benign_fixture_binds_same_route_shape() {
let path = "tests/dynamic_fixtures/js_frameworks/fastify/benign.js";
let bytes = std::fs::read(path).expect("fastify benign fixture exists");
let tree = parse_js(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
.expect("fastify adapter must bind benign fixture");
assert_eq!(binding.adapter, "js-fastify");
assert_eq!(binding.route.as_ref().unwrap().path, "/run");
}
#[test]
fn nest_vuln_fixture_binds_controller_route() {
let path = "tests/dynamic_fixtures/js_frameworks/nest/vuln.js";
let bytes = std::fs::read(path).expect("nest vuln fixture exists");
let tree = parse_js(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
.expect("nest adapter must bind");
assert_eq!(binding.adapter, "js-nest");
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.method, HttpMethod::GET);
let cmd_binding = binding
.request_params
.iter()
.find(|p| p.name == "cmd")
.expect("cmd formal");
match &cmd_binding.source {
ParamSource::QueryParam(q) => assert_eq!(q, "cmd"),
other => panic!("expected QueryParam(\"cmd\"), got {other:?}"),
}
}
#[test]
fn nest_benign_fixture_binds_same_route_shape() {
let path = "tests/dynamic_fixtures/js_frameworks/nest/benign.js";
let bytes = std::fs::read(path).expect("nest benign fixture exists");
let tree = parse_js(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
.expect("nest adapter must bind benign fixture");
assert_eq!(binding.adapter, "js-nest");
assert_eq!(binding.route.as_ref().unwrap().path, "/run");
}
#[test]
fn express_adapter_runs_before_fastify_for_express_files() {
// Regression guard: an Express file does not pull in `fastify`,
// so the Fastify adapter never fires. Registration order is
// alphabetical (`js-express` before `js-fastify`) which keeps the
// adapter dispatch deterministic.
let src: &[u8] = b"const express = require('express');\n\
const app = express();\n\
function h(req, res) { res.send('ok'); }\n\
app.get('/x', h);\n";
let tree = parse_js(src);
let summary = summary_for("h", "synthetic.js");
let binding =
detect_binding(&summary, tree.root_node(), src, Lang::JavaScript).expect("fires");
assert_eq!(binding.adapter, "js-express");
}

View file

@ -0,0 +1,68 @@
//! Phase 13 (Track L.11) — TypeScript framework adapter integration tests.
//!
//! Mirrors `tests/js_frameworks_corpus.rs` against the TS fixtures.
//! The Express / Koa / Fastify adapters are registered under
//! [`Lang::JavaScript`] only (TypeScript code paths share the JS
//! adapter via the Lang dispatch); the Nest adapter is registered
//! under both [`Lang::JavaScript`] and [`Lang::TypeScript`] because
//! Nest is TypeScript-first.
#![cfg(feature = "dynamic")]
use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource};
use nyx_scanner::evidence::EntryKind;
use nyx_scanner::summary::FuncSummary;
use nyx_scanner::symbol::Lang;
fn parse_ts(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang =
tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
fn summary_for(name: &str, file: &str) -> FuncSummary {
FuncSummary {
name: name.into(),
file_path: file.into(),
lang: "typescript".into(),
..Default::default()
}
}
#[test]
fn nest_ts_vuln_fixture_binds_controller_route() {
let path = "tests/dynamic_fixtures/ts_frameworks/nest/vuln.ts";
let bytes = std::fs::read(path).expect("nest TS vuln fixture exists");
let tree = parse_ts(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::TypeScript)
.expect("ts-nest adapter must bind");
assert_eq!(binding.adapter, "ts-nest");
assert_eq!(binding.kind, EntryKind::HttpRoute);
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.method, HttpMethod::GET);
let cmd_binding = binding
.request_params
.iter()
.find(|p| p.name == "cmd")
.expect("cmd formal");
match &cmd_binding.source {
ParamSource::QueryParam(q) => assert_eq!(q, "cmd"),
other => panic!("expected QueryParam, got {other:?}"),
}
}
#[test]
fn nest_ts_benign_fixture_binds_same_route_shape() {
let path = "tests/dynamic_fixtures/ts_frameworks/nest/benign.ts";
let bytes = std::fs::read(path).expect("nest TS benign fixture exists");
let tree = parse_ts(&bytes);
let summary = summary_for("runCmd", path);
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::TypeScript)
.expect("ts-nest adapter must bind benign fixture");
assert_eq!(binding.adapter, "ts-nest");
assert_eq!(binding.route.as_ref().unwrap().path, "/run");
}