[pitboss] phase 08: Track J.6 + Track L.6 — HEADER_INJECTION corpus + every HTTP framework

This commit is contained in:
pitboss 2026-05-18 01:08:32 -05:00
parent 59d627cb22
commit e0e49f65d3
45 changed files with 2552 additions and 41 deletions

View file

@ -0,0 +1,110 @@
//! Go [`super::super::FrameworkAdapter`] matching HTTP response-
//! header CRLF-injection sink constructions
//! (`http.ResponseWriter.Header().Set` / `Add`, Gin `c.Header`,
//! Echo `c.Response().Header().Set`).
//!
//! Phase 08 (Track J.6). Fires when the function body invokes one
//! of the canonical Go HTTP response writers and the surrounding
//! source imports `net/http` or one of the supported frameworks.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct HeaderGoAdapter;
const ADAPTER_NAME: &str = "header-go";
fn callee_is_header_setter(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(last, "Set" | "Add" | "Header" | "WriteHeader")
}
fn source_imports_go_http(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"\"net/http\"",
b"net/http\"",
b"github.com/gin-gonic/gin",
b"github.com/labstack/echo",
b"github.com/gofiber/fiber",
b"github.com/go-chi/chi",
b".Header().Set",
b".Header().Add",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for HeaderGoAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Go
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_source = source_imports_go_http(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Function,
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_go(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_header_set() {
let src: &[u8] =
b"package x\nimport \"net/http\"\nfunc Run(w http.ResponseWriter, v string) { w.Header().Set(\"Set-Cookie\", v) }\n";
let tree = parse_go(src);
let summary = FuncSummary {
name: "Run".into(),
callees: vec![crate::summary::CalleeSite::bare("Set")],
..Default::default()
};
assert!(HeaderGoAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_plain_function() {
let src: &[u8] = b"package x\nfunc Add(a, b int) int { return a + b }\n";
let tree = parse_go(src);
let summary = FuncSummary {
name: "Add".into(),
..Default::default()
};
assert!(HeaderGoAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
}

View file

@ -0,0 +1,106 @@
//! Java [`super::super::FrameworkAdapter`] matching HTTP response-
//! header CRLF-injection sink constructions
//! (`HttpServletResponse.setHeader` / `addHeader`).
//!
//! Phase 08 (Track J.6). Fires when the function body invokes one
//! of the canonical servlet response-writer entry points and the
//! surrounding source imports a servlet API.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct HeaderJavaAdapter;
const ADAPTER_NAME: &str = "header-java";
fn callee_is_header_setter(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(last, "setHeader" | "addHeader" | "setDateHeader" | "addDateHeader" | "setIntHeader" | "addIntHeader")
}
fn source_imports_servlet(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"javax.servlet",
b"jakarta.servlet",
b"HttpServletResponse",
b"ServerHttpResponse",
b"org.springframework.http",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for HeaderJavaAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Java
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_source = source_imports_servlet(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Function,
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_java(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_java::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_setheader() {
let src: &[u8] = b"import javax.servlet.http.HttpServletResponse;\n\
class C { void run(HttpServletResponse r, String v) { r.setHeader(\"Set-Cookie\", v); } }\n";
let tree = parse_java(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite::bare("setHeader")],
..Default::default()
};
assert!(HeaderJavaAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_plain_function() {
let src: &[u8] = b"class C { int add(int a, int b) { return a + b; } }\n";
let tree = parse_java(src);
let summary = FuncSummary {
name: "add".into(),
..Default::default()
};
assert!(HeaderJavaAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
}

View file

@ -0,0 +1,118 @@
//! JavaScript [`super::super::FrameworkAdapter`] matching HTTP
//! response-header CRLF-injection sink constructions
//! (`http.ServerResponse#setHeader`, Express `res.setHeader` /
//! `res.header`, Koa `ctx.set`).
//!
//! Phase 08 (Track J.6). Fires when the function body invokes one
//! of the canonical Node response writers and the surrounding source
//! imports the matching framework module or `node:http`.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct HeaderJsAdapter;
const ADAPTER_NAME: &str = "header-js";
fn callee_is_header_setter(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(last, "setHeader" | "header" | "set" | "writeHead" | "append")
}
fn source_uses_node_http(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"require('http')",
b"require(\"http\")",
b"require('node:http')",
b"from 'http'",
b"from \"http\"",
b"require('express')",
b"require(\"express\")",
b"from 'express'",
b"from \"express\"",
b"require('koa')",
b"require(\"koa\")",
b"require('fastify')",
b"require(\"fastify\")",
b"res.setHeader",
b"res.header",
b"ctx.set(",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for HeaderJsAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::JavaScript
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_source = source_uses_node_http(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Function,
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_setheader() {
let src: &[u8] = b"const http = require('http');\n\
function run(res, value) { res.setHeader('Set-Cookie', value); }\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite::bare("setHeader")],
..Default::default()
};
assert!(HeaderJsAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_plain_function() {
let src: &[u8] = b"function add(a, b) { return a + b; }\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "add".into(),
..Default::default()
};
assert!(HeaderJsAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
}

View file

@ -0,0 +1,109 @@
//! PHP [`super::super::FrameworkAdapter`] matching HTTP response-
//! header CRLF-injection sink constructions (`header()`,
//! Symfony / Laravel `Response::headers->set`).
//!
//! Phase 08 (Track J.6). Fires when the function body invokes one
//! of the canonical PHP response writers and the surrounding source
//! either references the built-in `$_SERVER` request surface or
//! imports a Symfony / Laravel response helper.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct HeaderPhpAdapter;
const ADAPTER_NAME: &str = "header-php";
fn callee_is_header_setter(name: &str) -> bool {
let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name);
let last = last.rsplit_once('.').map(|(_, s)| s).unwrap_or(last);
let last = last.rsplit_once("->").map(|(_, s)| s).unwrap_or(last);
matches!(last, "header" | "setRawHeader" | "headers" | "set" | "add")
}
fn source_uses_php_response(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"header(",
b"$_SERVER",
b"Symfony\\Component\\HttpFoundation",
b"Illuminate\\Http\\Response",
b"->headers->",
b"response()->header",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for HeaderPhpAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Php
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_source = source_uses_php_response(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Function,
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_php(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_php::LANGUAGE_PHP);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_header_call() {
let src: &[u8] = b"<?php\nfunction run($v) { header('Set-Cookie: ' . $v); }\n";
let tree = parse_php(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite::bare("header")],
..Default::default()
};
assert!(HeaderPhpAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_plain_function() {
let src: &[u8] = b"<?php\nfunction add($a, $b) { return $a + $b; }\n";
let tree = parse_php(src);
let summary = FuncSummary {
name: "add".into(),
..Default::default()
};
assert!(HeaderPhpAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
}

View file

@ -0,0 +1,112 @@
//! Python [`super::super::FrameworkAdapter`] matching HTTP response-
//! header CRLF-injection sink constructions
//! (`flask.Response.headers.__setitem__`, Django `HttpResponse.__setitem__`,
//! Starlette `headers.append`).
//!
//! Phase 08 (Track J.6). Fires when the function body invokes one
//! of the canonical Python web framework response writers and the
//! surrounding source imports the matching framework module.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct HeaderPythonAdapter;
const ADAPTER_NAME: &str = "header-python";
fn callee_is_header_setter(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"__setitem__" | "set_header" | "setdefault" | "add_header" | "append"
) || matches!(name, "Response.headers.__setitem__" | "make_response" | "Response.headers.add")
}
fn source_imports_python_web(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"from flask",
b"import flask",
b"from django.http",
b"from starlette",
b"from fastapi",
b"response.headers",
b"resp.headers",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for HeaderPythonAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Python
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_source = source_imports_python_web(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Function,
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_python(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_python::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_flask_header_assignment() {
let src: &[u8] = b"from flask import make_response\n\
def run(value):\n resp = make_response('hi')\n resp.headers['Set-Cookie'] = value\n return resp\n";
let tree = parse_python(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite::bare("__setitem__")],
..Default::default()
};
assert!(HeaderPythonAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_plain_function() {
let src: &[u8] = b"def add(a, b):\n return a + b\n";
let tree = parse_python(src);
let summary = FuncSummary {
name: "add".into(),
..Default::default()
};
assert!(HeaderPythonAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
}

View file

@ -0,0 +1,111 @@
//! Ruby [`super::super::FrameworkAdapter`] matching HTTP response-
//! header CRLF-injection sink constructions
//! (`Rack::Response#set_header`, Rails `response.headers[]=`,
//! Sinatra `response['Set-Cookie']=`).
//!
//! Phase 08 (Track J.6). Fires when the function body invokes one
//! of the canonical Ruby web framework response writers and the
//! surrounding source imports / mentions Rack / Rails / Sinatra.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct HeaderRubyAdapter;
const ADAPTER_NAME: &str = "header-ruby";
fn callee_is_header_setter(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
let last = last.rsplit_once('#').map(|(_, s)| s).unwrap_or(last);
matches!(last, "set_header" | "[]=" | "store" | "add_header")
}
fn source_uses_ruby_web(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"Rack::Response",
b"require 'rack'",
b"require \"rack\"",
b"require 'sinatra'",
b"require \"sinatra\"",
b"ActionController",
b"response.headers",
b"response[",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for HeaderRubyAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Ruby
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_source = source_uses_ruby_web(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Function,
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_ruby(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_ruby::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_set_header() {
let src: &[u8] = b"require 'rack'\n\
def run(value)\n response = Rack::Response.new\n response.set_header('Set-Cookie', value)\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite::bare("set_header")],
..Default::default()
};
assert!(HeaderRubyAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_plain_function() {
let src: &[u8] = b"def add(a, b)\n a + b\nend\n";
let tree = parse_ruby(src);
let summary = FuncSummary {
name: "add".into(),
..Default::default()
};
assert!(HeaderRubyAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
}

View file

@ -0,0 +1,112 @@
//! Rust [`super::super::FrameworkAdapter`] matching HTTP response-
//! header CRLF-injection sink constructions
//! (`axum`-style `headers_mut().insert`, `actix-web` `HttpResponse::
//! insert_header`, `hyper` `Response::headers_mut().insert`).
//!
//! Phase 08 (Track J.6). Fires when the function body invokes one
//! of the canonical Rust HTTP response header writers and the
//! surrounding source imports `http`, `axum`, `actix_web`, or
//! `hyper`.
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
pub struct HeaderRustAdapter;
const ADAPTER_NAME: &str = "header-rust";
fn callee_is_header_setter(name: &str) -> bool {
let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name);
let last = last.rsplit_once('.').map(|(_, s)| s).unwrap_or(last);
matches!(last, "insert" | "append" | "insert_header" | "header")
}
fn source_imports_rust_http(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"use http::HeaderMap",
b"use http::header",
b"use axum::",
b"use actix_web",
b"use hyper::",
b"HeaderMap::new",
b"HeaderValue::from",
b"headers_mut()",
];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
}
impl FrameworkAdapter for HeaderRustAdapter {
fn name(&self) -> &'static str {
ADAPTER_NAME
}
fn lang(&self) -> Lang {
Lang::Rust
}
fn detect(
&self,
summary: &FuncSummary,
_ast: tree_sitter::Node<'_>,
file_bytes: &[u8],
) -> Option<FrameworkBinding> {
let matches_call = super::any_callee_matches(summary, callee_is_header_setter);
let matches_source = source_imports_rust_http(file_bytes);
if matches_call && matches_source {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::Function,
route: None,
request_params: Vec::new(),
response_writer: None,
middleware: Vec::new(),
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_rust(src: &[u8]) -> tree_sitter::Tree {
let mut parser = tree_sitter::Parser::new();
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
parser.set_language(&lang).unwrap();
parser.parse(src, None).unwrap()
}
#[test]
fn fires_on_headers_insert() {
let src: &[u8] = b"use axum::http::HeaderMap;\n\
fn run(headers: &mut HeaderMap, value: &str) { headers.insert(\"set-cookie\", value.parse().unwrap()); }\n";
let tree = parse_rust(src);
let summary = FuncSummary {
name: "run".into(),
callees: vec![crate::summary::CalleeSite::bare("insert")],
..Default::default()
};
assert!(HeaderRustAdapter
.detect(&summary, tree.root_node(), src)
.is_some());
}
#[test]
fn skips_plain_function() {
let src: &[u8] = b"fn add(a: i32, b: i32) -> i32 { a + b }\n";
let tree = parse_rust(src);
let summary = FuncSummary {
name: "add".into(),
..Default::default()
};
assert!(HeaderRustAdapter
.detect(&summary, tree.root_node(), src)
.is_none());
}
}

View file

@ -11,6 +11,13 @@
//! the route / framework adapters; the per-cap sink adapters live
//! here so the per-language verticals can ship independently.
pub mod header_go;
pub mod header_java;
pub mod header_js;
pub mod header_php;
pub mod header_python;
pub mod header_ruby;
pub mod header_rust;
pub mod java_deserialize;
pub mod java_thymeleaf;
pub mod js_handlebars;
@ -33,6 +40,13 @@ pub mod xxe_php;
pub mod xxe_python;
pub mod xxe_ruby;
pub use header_go::HeaderGoAdapter;
pub use header_java::HeaderJavaAdapter;
pub use header_js::HeaderJsAdapter;
pub use header_php::HeaderPhpAdapter;
pub use header_python::HeaderPythonAdapter;
pub use header_ruby::HeaderRubyAdapter;
pub use header_rust::HeaderRustAdapter;
pub use java_deserialize::JavaDeserializeAdapter;
pub use java_thymeleaf::JavaThymeleafAdapter;
pub use js_handlebars::JsHandlebarsAdapter;

View file

@ -214,21 +214,20 @@ mod tests {
}
#[test]
fn registry_baseline_after_phase_07() {
// Phase 07 (Track J.5) adds the XPath-sink adapter for Java /
// Python / PHP / JavaScript, layered on top of the Phase 03
// deserialize + Phase 04 SSTI + Phase 05 XXE + Phase 06 LDAP
// adapters. Java / Python / PHP each grow from 4 → 5; the
// JavaScript slice grows from 1 (Handlebars only) → 2. Ruby
// still carries the 03+04+05 trio (no Ruby LDAP adapter); Go
// still has only the XXE adapter; Rust / C / Cpp / TypeScript
// still carry the Phase-01 empty baseline.
fn registry_baseline_after_phase_08() {
// Phase 08 (Track J.6) adds the header-injection adapter for
// every language carrying the HEADER_INJECTION corpus: Java /
// Python / PHP / Ruby / JavaScript / Go / Rust. Java /
// Python / PHP each grow from 5 → 6; Ruby from 3 → 4;
// JavaScript from 2 → 3; Go from 1 → 2; Rust from 0 → 1.
// C / Cpp / TypeScript still carry the Phase-01 empty
// baseline.
for lang in [Lang::Java, Lang::Python, Lang::Php] {
let registered = registry::adapters_for(lang);
assert_eq!(
registered.len(),
5,
"{:?} must have the J.1 deserialize + J.2 ssti + J.3 xxe + J.4 ldap + J.5 xpath adapters",
6,
"{:?} must have the J.1+J.2+J.3+J.4+J.5+J.6 adapters",
lang,
);
for adapter in registered {
@ -238,8 +237,8 @@ mod tests {
let ruby_registered = registry::adapters_for(Lang::Ruby);
assert_eq!(
ruby_registered.len(),
3,
"Ruby must still carry the J.1 deserialize + J.2 ssti + J.3 xxe adapters",
4,
"Ruby must have the J.1 + J.2 + J.3 + J.6 header adapters",
);
for adapter in ruby_registered {
assert_eq!(adapter.lang(), Lang::Ruby);
@ -247,8 +246,8 @@ mod tests {
let js_registered = registry::adapters_for(Lang::JavaScript);
assert_eq!(
js_registered.len(),
2,
"JavaScript must have the J.2 Handlebars + J.5 xpath-js adapters",
3,
"JavaScript must have J.2 Handlebars + J.5 xpath-js + J.6 header-js",
);
for adapter in js_registered {
assert_eq!(adapter.lang(), Lang::JavaScript);
@ -256,11 +255,20 @@ mod tests {
let go_registered = registry::adapters_for(Lang::Go);
assert_eq!(
go_registered.len(),
1,
"Go must have exactly the J.3 xxe-go adapter",
2,
"Go must have J.3 xxe-go + J.6 header-go",
);
assert_eq!(go_registered[0].lang(), Lang::Go);
for lang in [Lang::Rust, Lang::C, Lang::Cpp, Lang::TypeScript] {
for adapter in go_registered {
assert_eq!(adapter.lang(), Lang::Go);
}
let rust_registered = registry::adapters_for(Lang::Rust);
assert_eq!(
rust_registered.len(),
1,
"Rust must have exactly the J.6 header-rust adapter",
);
assert_eq!(rust_registered[0].lang(), Lang::Rust);
for lang in [Lang::C, Lang::Cpp, Lang::TypeScript] {
assert!(
registry::adapters_for(lang).is_empty(),
"{:?} should still have zero adapters before its Track-L phase",

View file

@ -44,18 +44,23 @@ pub fn adapters_for(lang: Lang) -> &'static [&'static dyn FrameworkAdapter] {
// listed in alphabetical order of [`FrameworkAdapter::name`] so a
// later phase that appends a new adapter cannot silently re-order
// the existing first-match.
static RUST: &[&dyn FrameworkAdapter] = &[];
static RUST: &[&dyn FrameworkAdapter] = &[&super::adapters::HeaderRustAdapter];
static C: &[&dyn FrameworkAdapter] = &[];
static CPP: &[&dyn FrameworkAdapter] = &[];
static JAVA: &[&dyn FrameworkAdapter] = &[
&super::adapters::HeaderJavaAdapter,
&super::adapters::JavaDeserializeAdapter,
&super::adapters::JavaThymeleafAdapter,
&super::adapters::LdapSpringAdapter,
&super::adapters::XpathJavaAdapter,
&super::adapters::XxeJavaAdapter,
];
static GO: &[&dyn FrameworkAdapter] = &[&super::adapters::XxeGoAdapter];
static GO: &[&dyn FrameworkAdapter] = &[
&super::adapters::HeaderGoAdapter,
&super::adapters::XxeGoAdapter,
];
static PHP: &[&dyn FrameworkAdapter] = &[
&super::adapters::HeaderPhpAdapter,
&super::adapters::LdapPhpAdapter,
&super::adapters::PhpTwigAdapter,
&super::adapters::PhpUnserializeAdapter,
@ -63,6 +68,7 @@ static PHP: &[&dyn FrameworkAdapter] = &[
&super::adapters::XxePhpAdapter,
];
static PYTHON: &[&dyn FrameworkAdapter] = &[
&super::adapters::HeaderPythonAdapter,
&super::adapters::LdapPythonAdapter,
&super::adapters::PythonJinja2Adapter,
&super::adapters::PythonPickleAdapter,
@ -70,12 +76,14 @@ static PYTHON: &[&dyn FrameworkAdapter] = &[
&super::adapters::XxePythonAdapter,
];
static RUBY: &[&dyn FrameworkAdapter] = &[
&super::adapters::HeaderRubyAdapter,
&super::adapters::RubyErbAdapter,
&super::adapters::RubyMarshalAdapter,
&super::adapters::XxeRubyAdapter,
];
static TYPESCRIPT: &[&dyn FrameworkAdapter] = &[];
static JAVASCRIPT: &[&dyn FrameworkAdapter] = &[
&super::adapters::HeaderJsAdapter,
&super::adapters::JsHandlebarsAdapter,
&super::adapters::XpathJsAdapter,
];