use crate::tests::{parse_ts, parse_tsx};
#[test]
fn sink_captures_arg_idents_for_bare_identifier() {
let info = parse_ts("const userInput = getInput();\neval(userInput);");
let sink = info
.security_sinks
.iter()
.find(|s| s.callee_path == "eval")
.expect("eval sink captured");
assert!(sink.arg_idents.iter().any(|n| n == "userInput"));
}
#[test]
fn sink_captures_arg_idents_through_member_and_concat() {
let info = parse_ts("el.innerHTML = \"<b>\" + data.value;");
let sink = info
.security_sinks
.iter()
.find(|s| s.callee_path == "el.innerHTML")
.expect("innerHTML sink captured");
assert!(sink.arg_idents.iter().any(|n| n == "data"));
assert!(!sink.arg_idents.iter().any(|n| n == "value"));
}
#[test]
fn sink_captures_direct_arg_source_paths() {
let info = parse_ts("logger.error(process.env.SECRET_KEY);");
let sink = info
.security_sinks
.iter()
.find(|s| s.callee_path == "logger.error")
.expect("logger sink captured");
assert!(
sink.arg_source_paths
.iter()
.any(|path| path == "process.env.SECRET_KEY")
);
assert!(
sink.arg_source_paths
.iter()
.any(|path| path == "process.env")
);
}
#[test]
fn sink_captures_typed_arg_idents_and_source_paths() {
let info = parse_ts(
"logger.error(secret as string);\nlogger.warn(process.env.API_TOKEN satisfies string);",
);
let error_sink = info
.security_sinks
.iter()
.find(|s| s.callee_path == "logger.error")
.expect("logger error sink captured");
assert!(error_sink.arg_idents.iter().any(|n| n == "secret"));
let warn_sink = info
.security_sinks
.iter()
.find(|s| s.callee_path == "logger.warn")
.expect("logger warn sink captured");
assert!(
warn_sink
.arg_source_paths
.iter()
.any(|path| path == "process.env")
);
}
#[test]
fn sink_captures_arg_idents_in_call_argument() {
let info = parse_ts("db.query(buildSql(userId));");
let sink = info
.security_sinks
.iter()
.find(|s| s.callee_path == "db.query")
.expect("query sink captured");
assert!(sink.arg_idents.iter().any(|n| n == "buildSql"));
assert!(sink.arg_idents.iter().any(|n| n == "userId"));
}
#[test]
fn sink_captures_arg_idents_in_tagged_template() {
let info = parse_ts("const q = sql`SELECT * FROM t WHERE id = ${id}`;");
let sink = info
.security_sinks
.iter()
.find(|s| s.callee_path == "sql")
.expect("tagged-template sink captured");
assert!(sink.arg_idents.iter().any(|n| n == "id"));
}
#[test]
fn sink_captures_arg_idents_in_jsx_attr() {
let info = parse_tsx("const C = () => <div dangerouslySetInnerHTML={markup} />;");
let sink = info
.security_sinks
.iter()
.find(|s| s.callee_path == "dangerouslySetInnerHTML")
.expect("jsx-attr sink captured");
assert!(sink.arg_idents.iter().any(|n| n == "markup"));
}
#[test]
fn direct_binding_records_object_path_as_source() {
let info = parse_ts("const id = req.query.id;");
assert!(
info.tainted_bindings
.iter()
.any(|b| b.local == "id" && b.source_path == "req.query")
);
}
#[test]
fn direct_binding_records_exact_dom_source_path() {
let info = parse_ts("const ref = document.referrer;\nconst name = window.name;");
assert!(
info.tainted_bindings
.iter()
.any(|b| b.local == "ref" && b.source_path == "document.referrer")
);
assert!(
info.tainted_bindings
.iter()
.any(|b| b.local == "name" && b.source_path == "window.name")
);
}
#[test]
fn helper_call_binding_records_returned_param_source() {
let info = parse_ts(
r"
function userId(req) {
return req.query.id;
}
const id = userId(request);
",
);
assert!(
info.tainted_bindings
.iter()
.any(|b| b.local == "id" && b.source_path == "request.query")
);
}
#[test]
fn helper_call_binding_records_hoisted_function_source() {
let info = parse_ts(
r"
const id = userId(request);
function userId(req) {
return req.query.id;
}
",
);
assert!(
info.tainted_bindings
.iter()
.any(|b| b.local == "id" && b.source_path == "request.query")
);
}
#[test]
fn helper_call_binding_records_arrow_helper_source() {
let info = parse_ts(
r"
const userId = (req) => req.query.id;
const id = userId(request);
",
);
assert!(
info.tainted_bindings
.iter()
.any(|b| b.local == "id" && b.source_path == "request.query")
);
}
#[test]
fn helper_call_binding_records_function_expression_source() {
let info = parse_ts(
r"
const userId = function (req) {
return req.query.id;
};
const id = userId(request);
",
);
assert!(
info.tainted_bindings
.iter()
.any(|b| b.local == "id" && b.source_path == "request.query")
);
}
#[test]
fn helper_call_binding_does_not_follow_shadowed_helper_name() {
let info = parse_ts(
r"
function userId(req) {
return req.query.id;
}
function route(userId, request) {
const id = userId(request);
new RegExp(id);
}
",
);
assert!(
info.tainted_bindings.iter().all(|b| b.local != "id"),
"a local parameter named userId must shadow the module helper"
);
}
#[test]
fn helper_call_binding_is_one_hop_only() {
let info = parse_ts(
r"
function userId(req) {
return req.query.id;
}
function wrapped(req) {
return userId(req);
}
const id = wrapped(request);
",
);
assert!(
info.tainted_bindings.iter().all(|b| b.local != "id"),
"helper calls returning another helper call are out of scope"
);
}
#[test]
fn helper_call_binding_does_not_keep_stale_overridden_function_summary() {
let info = parse_ts(
r#"
function userId(req) {
return req.query.id;
}
function userId(req) {
return "static";
}
const id = userId(request);
"#,
);
assert!(
info.tainted_bindings.iter().all(|b| b.local != "id"),
"a later same-module declaration must replace the helper summary"
);
}
#[test]
fn destructure_binding_records_full_init_path_as_source() {
let info = parse_ts("const { id, name } = req.body;");
for local in ["id", "name"] {
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == local)
.unwrap_or_else(|| panic!("tainted binding for {local}"));
assert_eq!(binding.source_path, "req.body");
}
}
#[test]
fn await_init_unwraps_to_member_object_path() {
let info = parse_ts("async function h() { const body = await ctx.req.json(); }");
assert!(info.tainted_bindings.iter().all(|b| b.local != "body"));
}
#[test]
fn literal_init_records_no_source_binding() {
let info = parse_ts("const x = 1;\nconst y = \"hello\";");
assert!(info.tainted_bindings.is_empty());
}
#[test]
fn route_callback_param_records_framework_source() {
let info = parse_ts("app.post('/run', (req) => { eval(req); });");
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "req")
.expect("route callback request param source");
assert_eq!(binding.source_path, "framework.request");
}
#[test]
fn route_callback_destructured_param_records_framework_source() {
let info = parse_ts("app.post('/run', ({ body }) => { eval(body); });");
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "body")
.expect("route callback destructured request param source");
assert_eq!(binding.source_path, "framework.request");
}
#[test]
fn next_route_handler_param_records_next_request_source() {
let info = parse_ts("export async function POST(request: Request) { eval(request); }");
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "request")
.expect("Next route request param source");
assert_eq!(binding.source_path, "next.request");
}
#[test]
fn server_action_form_data_param_records_next_source() {
let info =
parse_ts("const action = async (formData: FormData) => { 'use server'; eval(formData); };");
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "formData")
.expect("server action FormData param source");
assert_eq!(binding.source_path, "next.form-data");
}
#[test]
fn queue_process_callback_param_records_job_source() {
let info = parse_ts("queue.process(async (job) => { eval(job); });");
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "job")
.expect("queue process job param source");
assert_eq!(binding.source_path, "queue.job");
}
#[test]
fn queue_worker_constructor_param_records_job_source() {
let info = parse_ts("new Worker('email', async ({ data }) => { eval(data); });");
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "data")
.expect("BullMQ worker destructured job param source");
assert_eq!(binding.source_path, "queue.job");
}
#[test]
fn mcp_tool_callback_param_records_input_source() {
let info = parse_ts("server.tool('lookup', schema, async ({ city }) => { eval(city); });");
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "city")
.expect("MCP tool input param source");
assert_eq!(binding.source_path, "mcp.tool-input");
}
#[test]
fn graphql_resolver_second_args_param_records_source() {
let info = parse_ts(
r"
export const resolvers = {
Query: {
user(_parent, args) {
eval(args.id);
},
},
};
",
);
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "args")
.expect("GraphQL resolver args param source");
assert_eq!(binding.source_path, "graphql.args");
}
#[test]
fn graphql_resolver_destructured_args_param_records_source() {
let info = parse_ts(
r"
export const resolvers = {
Query: {
user(_parent, { id }) {
eval(id);
},
},
};
",
);
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "id")
.expect("GraphQL resolver destructured args source");
assert_eq!(binding.source_path, "graphql.args");
}
#[test]
fn trpc_procedure_destructured_input_records_source() {
let info = parse_ts(
r"
export const router = t.router({
user: t.procedure
.input(schema)
.query(({ input }) => {
eval(input.id);
}),
});
",
);
let binding = info
.tainted_bindings
.iter()
.find(|b| b.local == "input")
.expect("tRPC input source");
assert_eq!(binding.source_path, "trpc.input");
}
#[test]
fn non_trpc_query_callback_does_not_record_input_source() {
let info = parse_ts("db.query(({ input }) => { eval(input.id); });");
assert!(
info.tainted_bindings
.iter()
.all(|b| b.source_path != "trpc.input")
);
}