diff --git a/end2end/tests-new/express-mysql2-taint-tracking.test.mjs b/end2end/tests-new/express-mysql2-taint-tracking.test.mjs new file mode 100644 index 000000000..ff7c6c43a --- /dev/null +++ b/end2end/tests-new/express-mysql2-taint-tracking.test.mjs @@ -0,0 +1,135 @@ +import { spawn } from "child_process"; +import { resolve } from "path"; +import { test } from "node:test"; +import { equal, fail, match, doesNotMatch } from "node:assert"; +import { getRandomPort } from "./utils/get-port.mjs"; +import { timeout } from "./utils/timeout.mjs"; + +const pathToAppDir = resolve( + import.meta.dirname, + "../../sample-apps/express-mysql2-taint-tracking" +); + +const port = await getRandomPort(); +const port2 = await getRandomPort(); + +test("it blocks SQL injection through transformed input in blocking mode", async () => { + const server = spawn( + `node`, + ["--require", "@aikidosec/firewall/instrument", "./app.js", port], + { + cwd: pathToAppDir, + env: { + ...process.env, + AIKIDO_DEBUG: "true", + AIKIDO_BLOCK: "true", + }, + } + ); + + try { + server.on("error", (err) => { + fail(err.message); + }); + + let stdout = ""; + server.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + server.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for the server to start + await timeout(2000); + + const [sqlInjection, normalAdd] = await Promise.all([ + // Payload with whitespace and uppercase that gets transformed by .trim().toLowerCase() + fetch(`http://127.0.0.1:${port}/add`, { + method: "POST", + body: JSON.stringify({ + name: " Njuska'); DELETE FROM cats_taint;-- HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH ", + }), + headers: { "Content-Type": "application/json" }, + signal: AbortSignal.timeout(5000), + }), + fetch(`http://127.0.0.1:${port}/add`, { + method: "POST", + body: JSON.stringify({ name: "Miau" }), + headers: { "Content-Type": "application/json" }, + signal: AbortSignal.timeout(5000), + }), + ]); + + equal(sqlInjection.status, 500); + equal(normalAdd.status, 200); + match(stdout, /Starting agent/); + match(stderr, /Zen has blocked an SQL injection/); + } catch (err) { + fail(err); + } finally { + server.kill(); + } +}); + +test("it does not block in monitoring mode", async () => { + const server = spawn( + `node`, + ["--require", "@aikidosec/firewall/instrument", "./app.js", port2], + { + cwd: pathToAppDir, + env: { + ...process.env, + AIKIDO_DEBUG: "true", + AIKIDO_BLOCK: "false", + }, + } + ); + + try { + server.on("error", (err) => { + fail(err.message); + }); + + let stdout = ""; + server.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + server.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for the server to start + await timeout(2000); + + const [sqlInjection, normalAdd] = await Promise.all([ + fetch(`http://127.0.0.1:${port2}/add`, { + method: "POST", + body: JSON.stringify({ + name: " Njuska'); DELETE FROM cats_taint;-- HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH ", + }), + headers: { "Content-Type": "application/json" }, + signal: AbortSignal.timeout(5000), + }), + fetch(`http://127.0.0.1:${port2}/add`, { + method: "POST", + body: JSON.stringify({ name: "Miau" }), + headers: { "Content-Type": "application/json" }, + signal: AbortSignal.timeout(5000), + }), + ]); + + equal(sqlInjection.status, 200); + equal(normalAdd.status, 200); + match(stdout, /Starting agent/); + doesNotMatch(stderr, /Zen has blocked an SQL injection/); + } catch (err) { + fail(err); + } finally { + server.kill(); + } +}); diff --git a/instrumentation-wasm/src/js_transformer/mod.rs b/instrumentation-wasm/src/js_transformer/mod.rs index ebe0a37e5..64b9e347f 100644 --- a/instrumentation-wasm/src/js_transformer/mod.rs +++ b/instrumentation-wasm/src/js_transformer/mod.rs @@ -2,3 +2,4 @@ pub mod helpers; pub mod instructions; pub mod transformer; mod transformer_impl; +pub mod user_code_transformer; diff --git a/instrumentation-wasm/src/js_transformer/user_code_transformer.rs b/instrumentation-wasm/src/js_transformer/user_code_transformer.rs new file mode 100644 index 000000000..048643942 --- /dev/null +++ b/instrumentation-wasm/src/js_transformer/user_code_transformer.rs @@ -0,0 +1,560 @@ +use std::collections::HashSet; + +use oxc_allocator::Allocator; +use oxc_ast::ast::{ + Argument, AssignmentOperator, AssignmentTarget, BinaryOperator, ChainElement, Expression, + FormalParameterKind, FormalParameters, FunctionBody, ImportOrExportKind, Statement, + VariableDeclarationKind, +}; +use oxc_ast::{AstBuilder, NONE}; +use oxc_codegen::{Codegen, CodegenOptions, CommentOptions}; +use oxc_parser::Parser; +use oxc_semantic::SemanticBuilder; +use oxc_span::{SPAN, SourceType}; +use oxc_traverse::{Traverse, TraverseCtx, traverse_mut}; + +use super::helpers::select_sourcetype_based_on_enum::select_sourcetype_based_on_enum; +use super::transformer_impl::TraverseState; + +const WRAP_HOOK_NAME: &str = "__zen_wrapMethodCallResult"; +const CONCAT_HOOK_NAME: &str = "__zen_wrapConcat"; +const INSTRUMENT_IMPORT_SOURCE: &str = "@aikidosec/firewall/instrument/internals"; + +/// Methods to wrap with taint tracking in user code. +const METHODS_TO_WRAP: &[&str] = &[ + // String methods that transform content + "replace", + "replaceAll", + "slice", + "substring", + "substr", + "trim", + "trimStart", + "trimEnd", + "trimLeft", + "trimRight", + "toLowerCase", + "toUpperCase", + "toLocaleLowerCase", + "toLocaleUpperCase", + "normalize", + "repeat", + "padStart", + "padEnd", + "split", + "charAt", + "at", + // Array methods (for chains like .split('').reverse().join('')) + "join", + "reverse", +]; + +/// Entry point: transform user code to wrap method calls with taint tracking. +pub fn transform_user_code_str(code: &str, src_type: &str) -> Result { + let allocator = Allocator::default(); + + // Don't double-transform + if code.contains(WRAP_HOOK_NAME) || code.contains(CONCAT_HOOK_NAME) { + return Err("Code already contains taint tracking hook".to_string()); + } + + let source_type = select_sourcetype_based_on_enum(src_type); + + let mut parser_result = Parser::new(&allocator, code, source_type).parse(); + + if parser_result.panicked || !parser_result.errors.is_empty() { + return Err(format!( + "Error while parsing code: {:?}", + parser_result.errors + )); + } + + let program = &mut parser_result.program; + + let semantic = SemanticBuilder::new().build(program); + + if !semantic.errors.is_empty() { + return Err(format!( + "Error during semantic analysis: {:?}", + semantic.errors + )); + } + + let (scopes, _nodes) = semantic.semantic.into_scoping_and_nodes(); + + let ast_builder = AstBuilder::new(&allocator); + + let t = &mut UserCodeTransformer::new(&allocator, &ast_builder); + let state = TraverseState {}; + + traverse_mut(t, &allocator, program, scopes, state); + + if !t.has_method_transforms && !t.has_concat_transforms { + // No transforms found - return original code unchanged + return Ok(code.to_string()); + } + + // Collect which hooks need to be imported + let mut hooks: Vec<&str> = Vec::new(); + if t.has_method_transforms { + hooks.push(WRAP_HOOK_NAME); + } + if t.has_concat_transforms { + hooks.push(CONCAT_HOOK_NAME); + } + + // Insert import for the hooks that are used + insert_wrap_hook_import( + &source_type, + parser_result.module_record.has_module_syntax, + &allocator, + &ast_builder, + &mut program.body, + &hooks, + ); + + let js = Codegen::new() + .with_options(CodegenOptions { + comments: CommentOptions { + normal: true, + jsdoc: true, + annotation: true, + legal: oxc_codegen::LegalComment::Inline, + }, + minify: false, + ..CodegenOptions::default() + }) + .build(program); + + Ok(js.code) +} + +fn is_common_js(source_type: &SourceType, has_module_syntax: bool) -> bool { + if source_type.is_commonjs() { + return true; + } + if source_type.is_unambiguous() && !has_module_syntax { + return true; + } + false +} + +fn insert_wrap_hook_import<'a>( + source_type: &SourceType, + has_module_syntax: bool, + allocator: &'a Allocator, + builder: &'a AstBuilder, + body: &mut oxc_allocator::Vec<'a, Statement<'a>>, + hooks: &[&'a str], +) { + if is_common_js(source_type, has_module_syntax) { + // const { hook1, hook2 } = require("@aikidosec/firewall/instrument/internals"); + let mut require_args = builder.vec_with_capacity(1); + require_args.push(Argument::StringLiteral(builder.alloc_string_literal( + SPAN, + allocator.alloc_str(INSTRUMENT_IMPORT_SOURCE), + None, + ))); + + let mut binding_properties = builder.vec_with_capacity(hooks.len()); + for &hook in hooks { + binding_properties.push(builder.binding_property( + SPAN, + builder.property_key_static_identifier(SPAN, hook), + builder.binding_pattern_binding_identifier(SPAN, hook), + true, + false, + )); + } + + let mut declarations = builder.vec_with_capacity(1); + declarations.push(builder.variable_declarator( + SPAN, + VariableDeclarationKind::Const, + builder.binding_pattern_object_pattern(SPAN, binding_properties, NONE), + NONE, + Some(builder.expression_call( + SPAN, + builder.expression_identifier(SPAN, "require"), + NONE, + require_args, + false, + )), + false, + )); + + let var_declaration = Statement::VariableDeclaration(builder.alloc_variable_declaration( + SPAN, + VariableDeclarationKind::Const, + declarations, + false, + )); + + body.insert(0, var_declaration); + return; + } + + // ESM: import { hook1, hook2 } from "@aikidosec/firewall/instrument/internals"; + let mut specifiers = builder.vec_with_capacity(hooks.len()); + for &hook in hooks { + specifiers.push(builder.import_declaration_specifier_import_specifier( + SPAN, + builder.module_export_name_identifier_name(SPAN, hook), + builder.binding_identifier(SPAN, hook), + ImportOrExportKind::Value, + )); + } + + let import_stmt = Statement::ImportDeclaration(builder.alloc_import_declaration( + SPAN, + Some(specifiers), + builder.string_literal(SPAN, allocator.alloc_str(INSTRUMENT_IMPORT_SOURCE), None), + None, + NONE, + ImportOrExportKind::Value, + )); + + body.insert(0, import_stmt); +} + +pub struct UserCodeTransformer<'a> { + pub allocator: &'a Allocator, + pub ast_builder: &'a AstBuilder<'a>, + methods: HashSet<&'static str>, + pub has_method_transforms: bool, + pub has_concat_transforms: bool, +} + +impl<'a> UserCodeTransformer<'a> { + pub fn new(allocator: &'a Allocator, ast_builder: &'a AstBuilder<'a>) -> Self { + Self { + allocator, + ast_builder, + methods: METHODS_TO_WRAP.iter().copied().collect(), + has_method_transforms: false, + has_concat_transforms: false, + } + } + + fn is_target_method(&self, name: &str) -> bool { + self.methods.contains(name) + } + + /// Build: __zen_wrapMethodCallResult(subject, (__a) => __a.method(args)) + fn build_wrapped_call( + &self, + subject: Expression<'a>, + method_name: &str, + arguments: oxc_allocator::Vec<'a, Argument<'a>>, + optional: bool, + ) -> Expression<'a> { + let builder = self.ast_builder; + + // Build the inner call: __a.method(args) or __a?.method(args) + let inner_call = if optional { + // Build __a?.method(args) as a ChainExpression + // The ?. is on the member access, not the call + let inner_callee = + Expression::StaticMemberExpression(builder.alloc_static_member_expression( + SPAN, + builder.expression_identifier(SPAN, "__a"), + builder.identifier_name(SPAN, self.allocator.alloc_str(method_name)), + true, // optional member access: __a?.method + )); + let chain_element = builder.chain_element_call_expression( + SPAN, + inner_callee, + oxc_ast::NONE, + arguments, + false, // the call itself is not optional + ); + builder.expression_chain(SPAN, chain_element) + } else { + let inner_callee = + Expression::StaticMemberExpression(builder.alloc_static_member_expression( + SPAN, + builder.expression_identifier(SPAN, "__a"), + builder.identifier_name(SPAN, self.allocator.alloc_str(method_name)), + false, + )); + builder.expression_call(SPAN, inner_callee, oxc_ast::NONE, arguments, false) + }; + + // Build arrow function body: { return __a.method(args); } + // Use expression body for conciseness: (__a) => __a.method(args) + let mut body_stmts = builder.vec_with_capacity(1); + body_stmts.push(Statement::ExpressionStatement( + builder.alloc_expression_statement(SPAN, inner_call), + )); + + let body: oxc_allocator::Box<'a, FunctionBody<'a>> = + builder.alloc_function_body(SPAN, builder.vec(), body_stmts); + + // Build params: (__a) + let mut params_items = builder.vec_with_capacity(1); + params_items.push(builder.formal_parameter( + SPAN, + builder.vec(), + builder.binding_pattern_binding_identifier(SPAN, "__a"), + oxc_ast::NONE, // type_annotation + oxc_ast::NONE, // initializer + false, // optional + None, // accessibility + false, // readonly + false, // override + )); + + let params: oxc_allocator::Box<'a, FormalParameters<'a>> = builder.alloc_formal_parameters( + SPAN, + FormalParameterKind::ArrowFormalParameters, + params_items, + oxc_ast::NONE, + ); + + // Build arrow function: (__a) => __a.method(args) + let arrow = builder.expression_arrow_function( + SPAN, + true, // expression body (concise arrow) + false, + oxc_ast::NONE, + params, + oxc_ast::NONE, + body, + ); + + // Build outer call: __zen_wrapMethodCallResult(subject, arrow) + let mut outer_args = builder.vec_with_capacity(2); + outer_args.push(Argument::from(subject)); + outer_args.push(Argument::from(arrow)); + + builder.expression_call( + SPAN, + builder.expression_identifier(SPAN, WRAP_HOOK_NAME), + oxc_ast::NONE, + outer_args, + false, + ) + } + + /// Build: __zen_wrapConcat(arg1, arg2, ...) + fn build_concat_call(&self, arguments: oxc_allocator::Vec<'a, Argument<'a>>) -> Expression<'a> { + self.ast_builder.expression_call( + SPAN, + self.ast_builder + .expression_identifier(SPAN, CONCAT_HOOK_NAME), + oxc_ast::NONE, + arguments, + false, + ) + } +} + +impl<'a> Traverse<'a, TraverseState> for UserCodeTransformer<'a> { + fn exit_expression( + &mut self, + node: &mut Expression<'a>, + _ctx: &mut TraverseCtx<'a, TraverseState>, + ) { + enum TransformKind { + MethodCall, + ConcatMethodCall, + ChainMethodCall, + ChainConcatMethodCall, + BinaryAddition, + AssignmentAddition, + } + + // Determine what kind of transform to apply (read-only check) + let transform = match node { + Expression::CallExpression(call_expr) => match &call_expr.callee { + Expression::StaticMemberExpression(member_expr) => { + let name = member_expr.property.name.as_str(); + if name == "concat" { + Some(TransformKind::ConcatMethodCall) + } else if self.is_target_method(name) { + Some(TransformKind::MethodCall) + } else { + None + } + } + _ => None, + }, + Expression::ChainExpression(chain_expr) => match &chain_expr.expression { + ChainElement::CallExpression(call_expr) => match &call_expr.callee { + Expression::StaticMemberExpression(member_expr) => { + let name = member_expr.property.name.as_str(); + if name == "concat" { + Some(TransformKind::ChainConcatMethodCall) + } else if self.is_target_method(name) { + Some(TransformKind::ChainMethodCall) + } else { + None + } + } + _ => None, + }, + _ => None, + }, + Expression::BinaryExpression(bin_expr) + if bin_expr.operator == BinaryOperator::Addition => + { + Some(TransformKind::BinaryAddition) + } + Expression::AssignmentExpression(assign_expr) + if assign_expr.operator == AssignmentOperator::Addition + && matches!( + &assign_expr.left, + AssignmentTarget::AssignmentTargetIdentifier(_) + ) => + { + Some(TransformKind::AssignmentAddition) + } + _ => None, + }; + + let Some(transform) = transform else { + return; + }; + + let placeholder = self.ast_builder.expression_null_literal(SPAN); + let old_expr = std::mem::replace(node, placeholder); + + match transform { + TransformKind::MethodCall => { + let Expression::CallExpression(call_box) = old_expr else { + unreachable!() + }; + let call_expr = call_box.unbox(); + let Expression::StaticMemberExpression(member_box) = call_expr.callee else { + unreachable!() + }; + let member_expr = member_box.unbox(); + let method_name = member_expr.property.name.as_str(); + let optional = call_expr.optional || member_expr.optional; + let subject = member_expr.object; + let arguments = call_expr.arguments; + + *node = self.build_wrapped_call(subject, method_name, arguments, optional); + self.has_method_transforms = true; + } + + TransformKind::ConcatMethodCall => { + // str.concat(a, b) → __zen_wrapConcat(str, a, b) + let Expression::CallExpression(call_box) = old_expr else { + unreachable!() + }; + let call_expr = call_box.unbox(); + let Expression::StaticMemberExpression(member_box) = call_expr.callee else { + unreachable!() + }; + let member_expr = member_box.unbox(); + let subject = member_expr.object; + let arguments = call_expr.arguments; + + let mut new_args = self.ast_builder.vec_with_capacity(arguments.len() + 1); + new_args.push(Argument::from(subject)); + for arg in arguments.into_iter() { + new_args.push(arg); + } + + *node = self.build_concat_call(new_args); + self.has_concat_transforms = true; + } + + TransformKind::ChainMethodCall => { + // a.b?.method() → __zen_wrapMethodCallResult(a.b, (__a) => __a?.method()) + let Expression::ChainExpression(chain_box) = old_expr else { + unreachable!() + }; + let chain_expr = chain_box.unbox(); + let ChainElement::CallExpression(call_box) = chain_expr.expression else { + unreachable!() + }; + let call_expr = call_box.unbox(); + let Expression::StaticMemberExpression(member_box) = call_expr.callee else { + unreachable!() + }; + let member_expr = member_box.unbox(); + let method_name = member_expr.property.name.as_str(); + let subject = member_expr.object; + let arguments = call_expr.arguments; + + *node = self.build_wrapped_call(subject, method_name, arguments, true); + self.has_method_transforms = true; + } + + TransformKind::ChainConcatMethodCall => { + // a.b?.concat(x) → __zen_wrapConcat(a.b, x) + let Expression::ChainExpression(chain_box) = old_expr else { + unreachable!() + }; + let chain_expr = chain_box.unbox(); + let ChainElement::CallExpression(call_box) = chain_expr.expression else { + unreachable!() + }; + let call_expr = call_box.unbox(); + let Expression::StaticMemberExpression(member_box) = call_expr.callee else { + unreachable!() + }; + let member_expr = member_box.unbox(); + let subject = member_expr.object; + let arguments = call_expr.arguments; + + let mut new_args = self.ast_builder.vec_with_capacity(arguments.len() + 1); + new_args.push(Argument::from(subject)); + for arg in arguments.into_iter() { + new_args.push(arg); + } + + *node = self.build_concat_call(new_args); + self.has_concat_transforms = true; + } + + TransformKind::BinaryAddition => { + // a + b → __zen_wrapConcat(a, b) + let Expression::BinaryExpression(bin_box) = old_expr else { + unreachable!() + }; + let bin_expr = bin_box.unbox(); + + let mut args = self.ast_builder.vec_with_capacity(2); + args.push(Argument::from(bin_expr.left)); + args.push(Argument::from(bin_expr.right)); + + *node = self.build_concat_call(args); + self.has_concat_transforms = true; + } + + TransformKind::AssignmentAddition => { + // a += b → a = __zen_wrapConcat(a, b) + let Expression::AssignmentExpression(assign_box) = old_expr else { + unreachable!() + }; + let assign_expr = assign_box.unbox(); + + // Extract identifier name from target to create an expression + let AssignmentTarget::AssignmentTargetIdentifier(ref ident) = assign_expr.left + else { + unreachable!() + }; + let left_expr = self + .ast_builder + .expression_identifier(SPAN, self.allocator.alloc_str(ident.name.as_str())); + + let mut args = self.ast_builder.vec_with_capacity(2); + args.push(Argument::from(left_expr)); + args.push(Argument::from(assign_expr.right)); + + let concat_call = self.build_concat_call(args); + + *node = self.ast_builder.expression_assignment( + SPAN, + AssignmentOperator::Assign, + assign_expr.left, + concat_call, + ); + self.has_concat_transforms = true; + } + } + } +} diff --git a/instrumentation-wasm/src/wasm_bindings/mod.rs b/instrumentation-wasm/src/wasm_bindings/mod.rs index 6fb6312de..7363d16be 100644 --- a/instrumentation-wasm/src/wasm_bindings/mod.rs +++ b/instrumentation-wasm/src/wasm_bindings/mod.rs @@ -1,6 +1,7 @@ use wasm_bindgen::prelude::*; use crate::js_transformer::transformer::transform_code_str; +use crate::js_transformer::user_code_transformer::transform_user_code_str; #[wasm_bindgen] pub fn wasm_transform_code_str( @@ -12,3 +13,8 @@ pub fn wasm_transform_code_str( ) -> Result { transform_code_str(pkg_name, pkg_version, code, instructions_json, source_type) } + +#[wasm_bindgen] +pub fn wasm_transform_user_code(code: &str, source_type: &str) -> Result { + transform_user_code_str(code, source_type) +} diff --git a/library/agent/Context.ts b/library/agent/Context.ts index 378aa96cb..d2993bdaf 100644 --- a/library/agent/Context.ts +++ b/library/agent/Context.ts @@ -27,6 +27,12 @@ export type Context = { rawBody?: unknown; subdomains?: string[]; // https://expressjs.com/en/5x/api.html#req.subdomains markUnsafe?: unknown[]; + /** + * Maps transformed values back to their original source and payload. + * Used by taint tracking to attribute attacks through string transformations. + * Key: transformed string, Value: { source, payload (original value) } + */ + taintTracking?: Map; cache?: ReturnType; cachePathTraversal?: ReturnType; /** diff --git a/library/agent/hooks/instrumentation/injectedFunctions.ts b/library/agent/hooks/instrumentation/injectedFunctions.ts index 5b288c9b3..967b6d9a2 100644 --- a/library/agent/hooks/instrumentation/injectedFunctions.ts +++ b/library/agent/hooks/instrumentation/injectedFunctions.ts @@ -3,6 +3,8 @@ import { bindContext, getContext } from "../../Context"; import { inspectArgs } from "../wrapExport"; import { getFileCallbackInfo, getFunctionCallbackInfo } from "./instructions"; +export { __zen_wrapMethodCallResult, __zen_wrapConcat } from "./taintTracking"; + export function __instrumentInspectArgs( id: string, args: IArguments | unknown[], diff --git a/library/agent/hooks/instrumentation/loadHook.ts b/library/agent/hooks/instrumentation/loadHook.ts index 0fea1a277..cb101a4aa 100644 --- a/library/agent/hooks/instrumentation/loadHook.ts +++ b/library/agent/hooks/instrumentation/loadHook.ts @@ -3,6 +3,7 @@ import { getModuleInfoFromPath } from "../getModuleInfoFromPath"; import { isBuiltinModule } from "../isBuiltinModule"; import { getPackageVersionFromPath } from "./getPackageVersionFromPath"; import { transformCode } from "./codeTransformation"; +import { transformUserCode } from "./userCodeTransformation"; import { getPackageFileInstrumentationInstructions, shouldPatchBuiltin, @@ -71,9 +72,8 @@ function patchPackage( ) { const moduleInfo = getModuleInfoFromPath(path); if (!moduleInfo) { - // This is e.g. the case for user code (not a dependency) - // We don't want to modify user code yet - return previousLoadResult; + // User code (not a dependency) — apply taint tracking transformation + return patchUserCode(path, previousLoadResult); } // Check if the version of the package is supported @@ -188,6 +188,32 @@ function isSelfCheckImport(path: string) { .includes("hooks/instrumentation/zenHooksCheckImport."); // .js or .ts } +function patchUserCode( + path: string, + previousLoadResult: ReturnType +) { + const loadFormat = + (previousLoadResult.format as "commonjs" | "module" | undefined) ?? + "unambiguous"; + + const sourceString = + typeof previousLoadResult.source === "string" + ? previousLoadResult.source + : new TextDecoder("utf-8").decode(previousLoadResult.source); + + const newSource = transformUserCode(path, sourceString, loadFormat); + + if (!newSource || newSource === sourceString) { + return previousLoadResult; + } + + return { + format: previousLoadResult.format, + shortCircuit: previousLoadResult.shortCircuit, + source: newSource, + }; +} + function updateSelfCheckSource(previousLoadResult: ReturnType) { const sourceString = typeof previousLoadResult.source === "string" diff --git a/library/agent/hooks/instrumentation/taintTracking.test.ts b/library/agent/hooks/instrumentation/taintTracking.test.ts new file mode 100644 index 000000000..8bc80e6ec --- /dev/null +++ b/library/agent/hooks/instrumentation/taintTracking.test.ts @@ -0,0 +1,366 @@ +import * as t from "tap"; +import { Context, runWithContext } from "../../Context"; +import { __zen_wrapMethodCallResult, __zen_wrapConcat } from "./taintTracking"; +import { extractStringsFromUserInputCached } from "../../../helpers/extractStringsFromUserInputCached"; +import { getSourceForUserString } from "../../../helpers/getSourceForUserString"; +import { checkContextForSqlInjection } from "../../../vulnerabilities/sql-injection/checkContextForSqlInjection"; +import { SQLDialectMySQL } from "../../../vulnerabilities/sql-injection/dialects/SQLDialectMySQL"; + +function createContext(overrides?: Partial): Context { + return { + remoteAddress: "::1", + method: "GET", + url: "http://localhost", + query: {}, + headers: {}, + body: undefined, + cookies: {}, + routeParams: {}, + source: "express", + route: "/test", + ...overrides, + }; +} + +t.test("it returns the result without context", async (t) => { + // No context — hook should still execute the method and return the result + const result = __zen_wrapMethodCallResult("hello", (s) => + (s as string).toUpperCase() + ); + t.same(result, "HELLO"); +}); + +t.test("it returns the result when subject is not user input", async (t) => { + const context = createContext({ query: { name: "john" } }); + + runWithContext(context, () => { + const result = __zen_wrapMethodCallResult("not-user-input", (s) => + (s as string).toUpperCase() + ); + t.same(result, "NOT-USER-INPUT"); + + // Should NOT be tracked + const cache = extractStringsFromUserInputCached(context); + t.notOk(cache.has("NOT-USER-INPUT")); + }); +}); + +t.test("it tracks transformed value when subject is user input", async (t) => { + const context = createContext({ query: { name: "hello" } }); + + runWithContext(context, () => { + const result = __zen_wrapMethodCallResult("hello", (s) => + (s as string).toUpperCase() + ); + t.same(result, "HELLO"); + + // Transformed value should be in the cache + const cache = extractStringsFromUserInputCached(context); + t.ok(cache.has("HELLO")); + + // Source attribution should trace back to query + t.same(getSourceForUserString(context, "HELLO"), "query"); + }); +}); + +t.test("it tracks through a chain of transformations", async (t) => { + const context = createContext({ query: { name: " Hello " } }); + + runWithContext(context, () => { + // Simulate: name.trim().toLowerCase() + const trimmed = __zen_wrapMethodCallResult(" Hello ", (s) => + (s as string).trim() + ); + t.same(trimmed, "Hello"); + + const lowered = __zen_wrapMethodCallResult(trimmed, (s) => + (s as string).toLowerCase() + ); + t.same(lowered, "hello"); + + // Both intermediate and final values should be tracked + const cache = extractStringsFromUserInputCached(context); + t.ok(cache.has("Hello")); + t.ok(cache.has("hello")); + + // Source attribution traces back to query for all + t.same(getSourceForUserString(context, "Hello"), "query"); + t.same(getSourceForUserString(context, "hello"), "query"); + + // Original payload is preserved through the chain + t.same(context.taintTracking?.get("Hello")?.payload, " Hello "); + t.same(context.taintTracking?.get("hello")?.payload, " Hello "); + }); +}); + +t.test("it tracks split results (string → array)", async (t) => { + const context = createContext({ query: { csv: "a,b,c" } }); + + runWithContext(context, () => { + const parts = __zen_wrapMethodCallResult("a,b,c", (s) => + (s as string).split(",") + ); + t.same(parts, ["a", "b", "c"]); + + // Each array element should be tracked + const cache = extractStringsFromUserInputCached(context); + t.ok(cache.has("a")); + t.ok(cache.has("b")); + t.ok(cache.has("c")); + }); +}); + +t.test( + "it tracks array.reverse().join() chain (split-reverse-join)", + async (t) => { + const context = createContext({ query: { name: "hello" } }); + + runWithContext(context, () => { + // Simulate: name.split('').reverse().join('') + const chars = __zen_wrapMethodCallResult("hello", (s) => + (s as string).split("") + ) as string[]; + + const reversed = __zen_wrapMethodCallResult(chars, (a) => + [...(a as string[])].reverse() + ) as string[]; + + const joined = __zen_wrapMethodCallResult(reversed, (a) => + (a as string[]).join("") + ); + + t.same(joined, "olleh"); + + // Final reversed string should be tracked + const cache = extractStringsFromUserInputCached(context); + t.ok(cache.has("olleh")); + + // Source attribution traces back to query + t.same(getSourceForUserString(context, "olleh"), "query"); + }); + } +); + +t.test("it tracks replace transformations", async (t) => { + const context = createContext({ + query: { name: "O'Brien" }, + }); + + runWithContext(context, () => { + const escaped = __zen_wrapMethodCallResult("O'Brien", (s) => + (s as string).replace("'", "\\'") + ); + t.same(escaped, "O\\'Brien"); + + const cache = extractStringsFromUserInputCached(context); + t.ok(cache.has("O\\'Brien")); + t.same(getSourceForUserString(context, "O\\'Brien"), "query"); + }); +}); + +t.test("it attributes to the correct source", async (t) => { + const context = createContext({ + query: { q: "query-value" }, + body: { name: "body-value" }, + }); + + runWithContext(context, () => { + const fromQuery = __zen_wrapMethodCallResult("query-value", (s) => + (s as string).toUpperCase() + ); + const fromBody = __zen_wrapMethodCallResult("body-value", (s) => + (s as string).toUpperCase() + ); + + t.same(getSourceForUserString(context, fromQuery as string), "query"); + t.same(getSourceForUserString(context, fromBody as string), "body"); + }); +}); + +t.test("it does not break when method throws", async (t) => { + const context = createContext({ query: { name: "hello" } }); + + runWithContext(context, () => { + t.throws(() => { + __zen_wrapMethodCallResult("hello", () => { + throw new Error("method error"); + }); + }, /method error/); + }); +}); + +t.test("it ignores empty string results", async (t) => { + const context = createContext({ query: { name: "hello" } }); + + runWithContext(context, () => { + __zen_wrapMethodCallResult("hello", () => ""); + + // Empty string should NOT be tracked + const cache = extractStringsFromUserInputCached(context); + t.notOk(cache.has("")); + }); +}); + +t.test("it ignores non-string non-array results", async (t) => { + const context = createContext({ query: { name: "42" } }); + + runWithContext(context, () => { + const result = __zen_wrapMethodCallResult("42", () => 42); + t.same(result, 42); + // No crash, number result is just returned as-is + }); +}); + +t.test("it handles undefined subject (optional chaining)", async (t) => { + const context = createContext({ query: { name: "hello" } }); + + runWithContext(context, () => { + // Simulates: a.b?.trim() where a.b is undefined + const result = __zen_wrapMethodCallResult(undefined, (s) => + (s as any)?.trim() + ); + t.same(result, undefined); + }); +}); + +t.test("it handles null subject (optional chaining)", async (t) => { + const context = createContext({ query: { name: "hello" } }); + + runWithContext(context, () => { + // Simulates: a.b?.trim() where a.b is null + const result = __zen_wrapMethodCallResult(null, (s) => (s as any)?.trim()); + t.same(result, undefined); + }); +}); + +t.test( + "end-to-end: detects SQL injection through transformed input", + async (t) => { + const context = createContext({ + query: { search: " '; DROP TABLE users; -- " }, + }); + + runWithContext(context, () => { + // Without taint tracking, if the user transforms the input, + // Zen would not find the original value in the SQL query. + // The original has leading/trailing spaces, so after trim() + // the value differs from the original. Since SQL injection + // detection already lowercases both sides, toLowerCase() alone + // would not require taint tracking — but trim() does. + + // Simulate: search.trim() + const trimmed = __zen_wrapMethodCallResult( + " '; DROP TABLE users; -- ", + (s) => (s as string).trim() + ); + + // The transformed value is used in the SQL query + const sql = `SELECT * FROM items WHERE name = '${trimmed as string}'`; + + const result = checkContextForSqlInjection({ + sql, + operation: "mysql.query", + context, + dialect: new SQLDialectMySQL(), + }); + + t.ok(result, "SQL injection should be detected"); + if (result && "kind" in result) { + t.same(result.kind, "sql_injection"); + t.same(result.source, "query"); + } + }); + } +); + +// __zen_wrapConcat tests + +t.test("wrapConcat returns result without context", async (t) => { + const result = __zen_wrapConcat("hello", " world"); + t.same(result, "hello world"); +}); + +t.test("wrapConcat preserves number addition semantics", async (t) => { + const result = __zen_wrapConcat(1, 2); + t.same(result, 3); +}); + +t.test("wrapConcat tracks when left arg is tainted", async (t) => { + const context = createContext({ query: { name: "admin" } }); + + runWithContext(context, () => { + const result = __zen_wrapConcat("admin", " OR 1=1"); + t.same(result, "admin OR 1=1"); + + const cache = extractStringsFromUserInputCached(context); + t.ok(cache.has("admin OR 1=1")); + t.same(getSourceForUserString(context, "admin OR 1=1"), "query"); + }); +}); + +t.test("wrapConcat tracks when right arg is tainted", async (t) => { + const context = createContext({ query: { name: "admin" } }); + + runWithContext(context, () => { + const result = __zen_wrapConcat("Hello ", "admin"); + t.same(result, "Hello admin"); + + const cache = extractStringsFromUserInputCached(context); + t.ok(cache.has("Hello admin")); + t.same(getSourceForUserString(context, "Hello admin"), "query"); + }); +}); + +t.test("wrapConcat does not track when no arg is tainted", async (t) => { + const context = createContext({ query: { name: "admin" } }); + + runWithContext(context, () => { + const result = __zen_wrapConcat("hello", " world"); + t.same(result, "hello world"); + + const cache = extractStringsFromUserInputCached(context); + t.notOk(cache.has("hello world")); + }); +}); + +t.test("wrapConcat handles multiple args (concat scenario)", async (t) => { + const context = createContext({ query: { name: "evil" } }); + + runWithContext(context, () => { + const result = __zen_wrapConcat("prefix_", "evil", "_suffix"); + t.same(result, "prefix_evil_suffix"); + + const cache = extractStringsFromUserInputCached(context); + t.ok(cache.has("prefix_evil_suffix")); + t.same(getSourceForUserString(context, "prefix_evil_suffix"), "query"); + }); +}); + +t.test("wrapConcat ignores empty string result", async (t) => { + const context = createContext({ query: { name: "admin" } }); + + runWithContext(context, () => { + const result = __zen_wrapConcat("", ""); + t.same(result, ""); + + const cache = extractStringsFromUserInputCached(context); + t.notOk(cache.has("")); + }); +}); + +t.test("wrapConcat tracks through chain with method transform", async (t) => { + const context = createContext({ query: { name: " admin " } }); + + runWithContext(context, () => { + // Simulate: name.trim() + " OR 1=1" + const trimmed = __zen_wrapMethodCallResult(" admin ", (s) => + (s as string).trim() + ); + const result = __zen_wrapConcat(trimmed, " OR 1=1"); + t.same(result, "admin OR 1=1"); + + const cache = extractStringsFromUserInputCached(context); + t.ok(cache.has("admin OR 1=1")); + t.same(getSourceForUserString(context, "admin OR 1=1"), "query"); + }); +}); diff --git a/library/agent/hooks/instrumentation/taintTracking.ts b/library/agent/hooks/instrumentation/taintTracking.ts new file mode 100644 index 000000000..2d7ac511d --- /dev/null +++ b/library/agent/hooks/instrumentation/taintTracking.ts @@ -0,0 +1,174 @@ +import { getContext, type Context } from "../../Context"; +import { extractStringsFromUserInputCached } from "../../../helpers/extractStringsFromUserInputCached"; +import { getSourceForUserString } from "../../../helpers/getSourceForUserString"; +import type { Source } from "../../Source"; + +/** + * Runtime hook injected into user code to track tainted value transformations. + * + * When user code transforms a value that originated from user input (e.g. req.query), + * the original value is in the context's cache Set. After transformation, the new value + * wouldn't be in the set, so sinks can't detect attacks using it. + * + * This hook checks if the subject of a method call is a known user input, + * and if so, adds the result to the cache so sinks can check it too. + * + * Example transformation by the code transformer: + * name.replace("'", "") + * → __zen_wrapMethodCallResult(name, (__a) => __a.replace("'", "")) + */ +export function __zen_wrapMethodCallResult( + subject: unknown, + fn: (subject: unknown) => unknown +): unknown { + // Always execute the method — never break user code + const result = fn(subject); + + try { + const context = getContext() as Context | undefined; + if (!context) { + return result; + } + + const cache = extractStringsFromUserInputCached(context); + + if (typeof subject === "string" && subject.length > 0) { + if (cache.has(subject)) { + const sourceInfo = resolveSource(context, subject); + if (sourceInfo) { + trackResult(context, cache, result, sourceInfo); + } + } + } else if (Array.isArray(subject)) { + // For array subjects (e.g. .reverse() or .join() on a split result) + const sourceInfo = findArraySourceInfo(context, subject, cache); + if (sourceInfo) { + trackResult(context, cache, result, sourceInfo); + } + } + } catch { + // Never break user code due to taint tracking errors + } + + return result; +} + +/** + * Find the source and original payload for a string value. + * Checks the taint tracking map first (cheap), then falls back to source lookup. + */ +function resolveSource( + context: Context, + str: string +): { source: Source; payload: string } | undefined { + // Check taint tracking map first (handles chain propagation cheaply) + const tracked = context.taintTracking?.get(str); + if (tracked) { + return tracked; + } + + // Fall back to regular source lookup (first value in a chain) + const source = getSourceForUserString(context, str); + if (source) { + return { source, payload: str }; + } + + return undefined; +} + +/** + * Check if any string element in the array is tainted. + * Returns the source info for the first tainted element found. + */ +function findArraySourceInfo( + context: Context, + arr: unknown[], + cache: Set +): { source: Source; payload: string } | undefined { + for (const item of arr) { + if (typeof item === "string" && item.length > 0 && cache.has(item)) { + return resolveSource(context, item); + } + } + return undefined; +} + +function trackResult( + context: Context, + cache: Set, + result: unknown, + sourceInfo: { source: Source; payload: string } +): void { + if (typeof result === "string" && result.length > 0) { + cache.add(result); + addToTaintTracking(context, result, sourceInfo); + return; + } + + if (Array.isArray(result)) { + for (const item of result) { + if (typeof item === "string" && item.length > 0) { + cache.add(item); + addToTaintTracking(context, item, sourceInfo); + } + } + } +} + +function addToTaintTracking( + context: Context, + value: string, + sourceInfo: { source: Source; payload: string } +): void { + if (!context.taintTracking) { + context.taintTracking = new Map(); + } + context.taintTracking.set(value, sourceInfo); +} + +/** + * Runtime hook injected into user code to track tainted value through concatenation. + * + * Handles: + * a + b → __zen_wrapConcat(a, b) + * a += b → a = __zen_wrapConcat(a, b) + * str.concat(…) → __zen_wrapConcat(str, a, b, …) + * + * Checks every argument: if any is a known user input, the string result is tracked. + */ +export function __zen_wrapConcat(...args: unknown[]): unknown { + // Compute the concatenation result using + to preserve JS semantics + let result: unknown = args[0]; + for (let i = 1; i < args.length; i++) { + result = (result as any) + (args[i] as any); + } + + try { + if (typeof result !== "string" || result.length === 0) { + return result; + } + + const context = getContext() as Context | undefined; + if (!context) { + return result; + } + + const cache = extractStringsFromUserInputCached(context); + + // Check if any argument is tainted + for (const arg of args) { + if (typeof arg === "string" && arg.length > 0 && cache.has(arg)) { + const sourceInfo = resolveSource(context, arg); + if (sourceInfo) { + cache.add(result); + addToTaintTracking(context, result, sourceInfo); + return result; + } + } + } + } catch { + // Never break user code due to taint tracking errors + } + + return result; +} diff --git a/library/agent/hooks/instrumentation/userCodeTransformation.test.ts b/library/agent/hooks/instrumentation/userCodeTransformation.test.ts new file mode 100644 index 000000000..3c51b6bc7 --- /dev/null +++ b/library/agent/hooks/instrumentation/userCodeTransformation.test.ts @@ -0,0 +1,389 @@ +import * as t from "tap"; +import { transformUserCode } from "./userCodeTransformation"; + +const compareCodeStrings = (code1: string, code2: string) => { + return code1.replace(/\s+/g, "") === code2.replace(/\s+/g, ""); +}; + +t.before(() => { + process.env.AIKIDO_TEST_NEW_INSTRUMENTATION = "false"; +}); + +t.after(() => { + process.env.AIKIDO_TEST_NEW_INSTRUMENTATION = "true"; +}); + +const isSameCode = (t: any, actual: string, expected: string) => { + t.same( + compareCodeStrings(actual, expected), + true, + `Expected:\n${expected}\n\nGot:\n${actual}` + ); +}; + +t.test("wraps simple method call (ESM)", async (t) => { + const result = transformUserCode( + "app.js", + ` + import express from "express"; + const name = req.query.name; + const upper = name.toUpperCase(); + `, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + import express from "express"; + const name = req.query.name; + const upper = __zen_wrapMethodCallResult(name, (__a) => __a.toUpperCase());` + ); +}); + +t.test("wraps simple method call (CJS)", async (t) => { + const result = transformUserCode( + "app.js", + ` + const express = require("express"); + const name = req.query.name; + const upper = name.toUpperCase(); + `, + "commonjs" + ); + + t.ok(result); + isSameCode( + t, + result!, + `const { __zen_wrapMethodCallResult } = require("@aikidosec/firewall/instrument/internals"); + const express = require("express"); + const name = req.query.name; + const upper = __zen_wrapMethodCallResult(name, (__a) => __a.toUpperCase());` + ); +}); + +t.test("wraps chained method calls", async (t) => { + const result = transformUserCode( + "app.js", + `const cleaned = name.trim().toLowerCase();`, + "module" + ); + + t.ok(result); + // exit_expression (bottom-up) means trim() is wrapped first, then toLowerCase() + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + const cleaned = __zen_wrapMethodCallResult(__zen_wrapMethodCallResult(name, (__a) => __a.trim()), (__a) => __a.toLowerCase());` + ); +}); + +t.test("wraps method calls with arguments", async (t) => { + const result = transformUserCode( + "app.js", + `const escaped = input.replace("'", "\\\\'");`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + const escaped = __zen_wrapMethodCallResult(input, (__a) => __a.replace("'", "\\\\'"));` + ); +}); + +t.test("wraps split and join", async (t) => { + const result = transformUserCode( + "app.js", + `const reversed = name.split("").reverse().join("");`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + const reversed = __zen_wrapMethodCallResult(__zen_wrapMethodCallResult(__zen_wrapMethodCallResult(name, (__a) => __a.split("")), (__a) => __a.reverse()), (__a) => __a.join(""));` + ); +}); + +t.test("does not wrap non-target methods", async (t) => { + const code = ` + const result = obj.customMethod(); + console.log("hello"); + arr.push(1); + str.indexOf("x"); + `; + const result = transformUserCode("app.js", code, "module"); + + // No target methods, so result should be the original code + t.ok(result); + isSameCode(t, result!, code); +}); + +t.test("wraps multiple different method calls", async (t) => { + const result = transformUserCode( + "app.js", + ` + const a = x.trim(); + const b = y.toLowerCase(); + const c = z.slice(0, 5); + `, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + const a = __zen_wrapMethodCallResult(x, (__a) => __a.trim()); + const b = __zen_wrapMethodCallResult(y, (__a) => __a.toLowerCase()); + const c = __zen_wrapMethodCallResult(z, (__a) => __a.slice(0, 5));` + ); +}); + +t.test("handles complex subject expressions", async (t) => { + const result = transformUserCode( + "app.js", + `const upper = getInput().toUpperCase();`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + const upper = __zen_wrapMethodCallResult(getInput(), (__a) => __a.toUpperCase());` + ); +}); + +t.test("handles member expression subject", async (t) => { + const result = transformUserCode( + "app.js", + `const upper = req.query.name.toUpperCase();`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + const upper = __zen_wrapMethodCallResult(req.query.name, (__a) => __a.toUpperCase());` + ); +}); + +t.test("returns undefined for unparseable code", async (t) => { + const result = transformUserCode("app.js", `const { = broken`, "module"); + t.same(result, undefined); +}); + +t.test("rejects already-transformed code", async (t) => { + const result = transformUserCode( + "app.js", + `__zen_wrapMethodCallResult(x, (__a) => __a.trim())`, + "module" + ); + t.same(result, undefined); +}); + +t.test("unambiguous mode with ESM syntax", async (t) => { + const result = transformUserCode( + "app.js", + ` + import { foo } from "bar"; + const x = name.trim(); + `, + "unambiguous" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + import { foo } from "bar"; + const x = __zen_wrapMethodCallResult(name, (__a) => __a.trim());` + ); +}); + +t.test("unambiguous mode with CJS syntax", async (t) => { + const result = transformUserCode( + "app.js", + ` + const foo = require("bar"); + const x = name.trim(); + `, + "unambiguous" + ); + + t.ok(result); + isSameCode( + t, + result!, + `const { __zen_wrapMethodCallResult } = require("@aikidosec/firewall/instrument/internals"); + const foo = require("bar"); + const x = __zen_wrapMethodCallResult(name, (__a) => __a.trim());` + ); +}); + +t.test("wraps optional chaining method call", async (t) => { + const result = transformUserCode( + "app.js", + `const x = a.b?.trim();`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + const x = __zen_wrapMethodCallResult(a.b, (__a) => __a?.trim());` + ); +}); + +t.test("wraps chained optional method calls", async (t) => { + const result = transformUserCode( + "app.js", + `const x = a.b?.trim()?.toLowerCase();`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult } from "@aikidosec/firewall/instrument/internals"; + const x = __zen_wrapMethodCallResult(__zen_wrapMethodCallResult(a.b, (__a) => __a?.trim()), (__a) => __a?.toLowerCase());` + ); +}); + +t.test("wraps binary + operator", async (t) => { + const result = transformUserCode( + "app.js", + `const greeting = "Hello " + name;`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapConcat } from "@aikidosec/firewall/instrument/internals"; + const greeting = __zen_wrapConcat("Hello ", name);` + ); +}); + +t.test("wraps chained + operators", async (t) => { + const result = transformUserCode( + "app.js", + `const sql = "SELECT * FROM " + table + " WHERE 1";`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapConcat } from "@aikidosec/firewall/instrument/internals"; + const sql = __zen_wrapConcat(__zen_wrapConcat("SELECT * FROM ", table), " WHERE 1");` + ); +}); + +t.test("wraps += assignment (identifier)", async (t) => { + const result = transformUserCode( + "app.js", + `let str = "Hello"; str += name;`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapConcat } from "@aikidosec/firewall/instrument/internals"; + let str = "Hello"; str = __zen_wrapConcat(str, name);` + ); +}); + +t.test("wraps .concat() method call", async (t) => { + const result = transformUserCode( + "app.js", + `const result = str.concat(a, b);`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapConcat } from "@aikidosec/firewall/instrument/internals"; + const result = __zen_wrapConcat(str, a, b);` + ); +}); + +t.test("wraps .concat() with single argument", async (t) => { + const result = transformUserCode( + "app.js", + `const result = str.concat(other);`, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapConcat } from "@aikidosec/firewall/instrument/internals"; + const result = __zen_wrapConcat(str, other);` + ); +}); + +t.test("imports both hooks when needed", async (t) => { + const result = transformUserCode( + "app.js", + ` + const upper = name.toUpperCase(); + const greeting = "Hello " + upper; + `, + "module" + ); + + t.ok(result); + isSameCode( + t, + result!, + `import { __zen_wrapMethodCallResult, __zen_wrapConcat } from "@aikidosec/firewall/instrument/internals"; + const upper = __zen_wrapMethodCallResult(name, (__a) => __a.toUpperCase()); + const greeting = __zen_wrapConcat("Hello ", upper);` + ); +}); + +t.test("wraps + in CJS mode", async (t) => { + const result = transformUserCode( + "app.js", + ` + const express = require("express"); + const greeting = "Hello " + name; + `, + "commonjs" + ); + + t.ok(result); + isSameCode( + t, + result!, + `const { __zen_wrapConcat } = require("@aikidosec/firewall/instrument/internals"); + const express = require("express"); + const greeting = __zen_wrapConcat("Hello ", name);` + ); +}); diff --git a/library/agent/hooks/instrumentation/userCodeTransformation.ts b/library/agent/hooks/instrumentation/userCodeTransformation.ts new file mode 100644 index 000000000..43fd627bf --- /dev/null +++ b/library/agent/hooks/instrumentation/userCodeTransformation.ts @@ -0,0 +1,34 @@ +import { wasm_transform_user_code } from "./wasm/node_code_instrumentation"; +import { getSourceType, PackageLoadFormat } from "./getSourceType"; +import { join } from "path"; +import { isNewInstrumentationUnitTest } from "../../../helpers/isNewInstrumentationUnitTest"; +import { isEsmUnitTest } from "../../../helpers/isEsmUnitTest"; + +export function transformUserCode( + path: string, + code: string, + loadFormat: PackageLoadFormat +): string | undefined { + try { + const result = wasm_transform_user_code( + code, + getSourceType(path, loadFormat) + ); + + // Rewrite import path for unit tests if environment variable is set to true + if (isNewInstrumentationUnitTest()) { + return result.replace( + "@aikidosec/firewall/instrument/internals", + join( + __dirname, + isEsmUnitTest() ? "injectedFunctions.js" : "injectedFunctions.ts" + ) + ); + } + + return result; + } catch { + // Don't break user code loading if transformation fails + return undefined; + } +} diff --git a/library/helpers/getSourceForUserString.ts b/library/helpers/getSourceForUserString.ts index bcca7878c..d07b58eaf 100644 --- a/library/helpers/getSourceForUserString.ts +++ b/library/helpers/getSourceForUserString.ts @@ -18,5 +18,11 @@ export function getSourceForUserString( } } + // Check taint tracking map (for transformed user input values) + const taintInfo = context.taintTracking?.get(str); + if (taintInfo) { + return taintInfo.source; + } + return undefined; } diff --git a/sample-apps/express-mysql2-taint-tracking/app.js b/sample-apps/express-mysql2-taint-tracking/app.js new file mode 100644 index 000000000..20d0e2be1 --- /dev/null +++ b/sample-apps/express-mysql2-taint-tracking/app.js @@ -0,0 +1,72 @@ +const Zen = require("@aikidosec/firewall"); +const express = require("express"); +const mysql = require("mysql2/promise"); + +async function createConnection() { + const connection = await mysql.createConnection({ + host: "localhost", + user: "root", + password: "mypassword", + database: "catsdb", + port: 27015, + multipleStatements: true, + }); + + await connection.execute( + `CREATE TABLE IF NOT EXISTS cats_taint (petname varchar(255));` + ); + + return connection; +} + +async function main() { + const db = await createConnection(); + const app = express(); + app.use(express.json()); + + // This route transforms user input through a chain of string methods. + // Without taint tracking, Zen would not detect the SQL injection because + // the transformed value differs from the original user input. + // + // Example payload: " Njuska'); DELETE FROM cats_taint;-- HHHH... " + // .trimStart() → "Njuska'); DELETE FROM cats_taint;-- HHHH... " + // .trimEnd() → "Njuska'); DELETE FROM cats_taint;-- HHHH..." + // .toLowerCase()→ "njuska'); delete from cats_taint;-- hhhh..." + // .normalize() → (same) + // .replace() → "njuska'); delete from cats_taint;-- hhhh..." + // .split/.join → (same) + // .slice(0, 40) → "njuska'); delete from cats_taint;-- hhhh" + // .trim() → (same) + // + // SQL: INSERT INTO cats_taint (petname) VALUES ('njuska'); delete from cats_taint;-- hhhh'); + app.post("/add", async (req, res) => { + const name = req.body.name + .trimStart() + .trimEnd() + .toLowerCase() + .normalize("NFC") + .replace(/\s+/g, " ") + .split(" ") + .join(" ") + .slice(0, 40) + .trim(); + + // This is unsafe! For demo purposes only. + await db.query(`INSERT INTO cats_taint (petname) VALUES ('${name}');`); + + res.json({ success: true }); + }); + + app.get("/clear", async (req, res) => { + await db.execute("DELETE FROM cats_taint;"); + res.json({ success: true }); + }); + + const port = parseInt(process.argv[2], 10) || 4000; + + app.listen(port, () => { + console.log(`Listening on port ${port}`); + }); +} + +main(); diff --git a/sample-apps/express-mysql2-taint-tracking/package-lock.json b/sample-apps/express-mysql2-taint-tracking/package-lock.json new file mode 100644 index 000000000..6963dc2cc --- /dev/null +++ b/sample-apps/express-mysql2-taint-tracking/package-lock.json @@ -0,0 +1,937 @@ +{ + "name": "express-mysql2-taint-tracking", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "express-mysql2-taint-tracking", + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "express": "^5.0.0", + "mysql2": "^3.10.0" + } + }, + "../../build": { + "version": "0.0.0", + "license": "AGPL-3.0-or-later", + "engines": { + "node": ">=16" + } + }, + "node_modules/@aikidosec/firewall": { + "resolved": "../../build", + "link": true + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.3.tgz", + "integrity": "sha512-+3XhQEt4FEFuvGV0JjIDj4eP2OT/oIj/54dYvqhblnSzlfcxVOuj+cd15Xz6hsG4HU1a+A5+BA9gm0618C4z7A==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.3", + "named-placeholders": "^1.1.6", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.3" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/sample-apps/express-mysql2-taint-tracking/package.json b/sample-apps/express-mysql2-taint-tracking/package.json new file mode 100644 index 000000000..4c7c11a2e --- /dev/null +++ b/sample-apps/express-mysql2-taint-tracking/package.json @@ -0,0 +1,9 @@ +{ + "name": "express-mysql2-taint-tracking", + "private": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "express": "^5.0.0", + "mysql2": "^3.10.0" + } +}