feat(dynamic): enhance corpus sync script with improved payload parsing, registry checks, and expanded validation logic

This commit is contained in:
elipeter 2026-06-01 22:51:05 -05:00
parent 467d41dcfb
commit 8ee6e3af7c
22 changed files with 810 additions and 230 deletions

View file

@ -2544,6 +2544,37 @@ pub(super) fn def_use(
}
}
}
// Java `enhanced_for_statement` binds the loop variable on the
// `name` field and the iterable on the `value` field; Ruby's
// `for x in coll` uses `pattern`/`value`. Neither uses the
// JS/Python `left`/`right` convention, so without this mapping
// the loop binding was never recorded as a define and taint on
// the iterable could not reach the loop variable (OWASP's
// dominant `for (Cookie c : req.getCookies())` shape).
if left.is_none() && right.is_none() {
if let Some(v) = ast.child_by_field_name("value") {
left = ast
.child_by_field_name("name")
.or_else(|| ast.child_by_field_name("pattern"));
right = Some(v);
}
}
// PHP `foreach ($coll as $v)` / `foreach ($coll as $k => $v)`:
// the iterable and binding are unnamed children separated by the
// `as` keyword (only `body` is a named field). Map the binding
// onto `left` and the iterable onto `right` so the shared
// define/use logic below records the loop variable.
if left.is_none() && right.is_none() && ast.kind() == "foreach_statement" {
let mut cursor = ast.walk();
let kids: Vec<Node> = ast.children(&mut cursor).collect();
if let Some(as_pos) = kids.iter().position(|c| c.kind() == "as") {
right = kids[..as_pos].iter().rev().find(|c| c.is_named()).copied();
left = kids[as_pos + 1..]
.iter()
.find(|c| c.is_named() && lookup(lang, c.kind()) != Kind::Block)
.copied();
}
}
if left.is_none() && right.is_none() {
// C-style for, defer to default ident collection.
let mut idents = Vec::new();

View file

@ -2067,6 +2067,32 @@ fn is_binary_expr_kind(kind: &str, lang: &str) -> bool {
}
}
/// Classification text for a for-each loop's iterable expression.
///
/// Subscript / index iterables (`$_GET['x']`, `params[:list]`, `arr[i]`)
/// classify on their **base object**: taint sources are keyed on the base
/// name (`$_GET`, `params`), and the trailing index would otherwise break
/// the word-boundary suffix match in `classify`. Non-subscript iterables
/// (method calls, member chains, bare identifiers) use their full text.
fn iterable_label_text(iter: Node, code: &[u8]) -> Option<String> {
if matches!(
iter.kind(),
"subscript_expression" | "subscript" | "index_expression" | "element_reference"
) {
let base = iter
.child_by_field_name("object")
.or_else(|| iter.child_by_field_name("operand"))
.or_else(|| iter.child_by_field_name("value"))
.or_else(|| iter.child(0));
if let Some(b) = base
&& let Some(t) = text_of(b, code)
{
return Some(t);
}
}
text_of(iter, code)
}
/// Create a node in one short borrow and optionally attach a taint label.
#[allow(clippy::too_many_arguments)]
pub(super) fn push_node<'a>(
@ -2208,6 +2234,51 @@ pub(super) fn push_node<'a>(
text = iter_text;
}
// Java `for (T x : iter)`: tree-sitter-java emits `enhanced_for_statement`
// with the iterable on the `value` field. Classify against the iterable
// text so a source-returning call (`req.getCookies()`,
// `req.getParameterValues(..)`) lights up a Source on the loop node and
// the loop binding inherits its taint — the same loop-binding-inherits-
// iterator-taint contract the JS/Python rewrites above provide. The
// loop variable itself is recorded as a define by `def_use`'s Kind::For
// arm (via the `name`/`value` mapping), so the Source-labeled loop node
// taints the binding directly.
if lang == "java"
&& ast.kind() == "enhanced_for_statement"
&& let Some(value) = ast.child_by_field_name("value")
&& let Some(iter_text) = iterable_label_text(value, code)
{
text = iter_text;
}
// PHP `foreach ($iter as $v)` / `foreach ($iter as $k => $v)`: the
// iterable is the named child immediately preceding the `as` keyword
// (only `body` is a named field). Classify against the iterable text so
// a superglobal/source iterable (`$_GET[..]`, `$_POST[..]`) taints the
// loop binding, matching the JS/Python/Java rewrites.
if lang == "php" && ast.kind() == "foreach_statement" {
let mut cursor = ast.walk();
let kids: Vec<Node> = ast.children(&mut cursor).collect();
if let Some(as_pos) = kids.iter().position(|c| c.kind() == "as")
&& let Some(iter_node) = kids[..as_pos].iter().rev().find(|c| c.is_named()).copied()
&& let Some(iter_text) = iterable_label_text(iter_node, code)
{
text = iter_text;
}
}
// Ruby `for x in coll`: tree-sitter-ruby's `for` node carries the
// iterable on the `value` field. (The idiomatic `coll.each { |x| }`
// form is a method call with a block and is handled by the call/block
// machinery, not here.)
if lang == "ruby"
&& ast.kind() == "for"
&& let Some(value) = ast.child_by_field_name("value")
&& let Some(iter_text) = iterable_label_text(value, code)
{
text = iter_text;
}
// If this is a declaration/expression wrapper or an assignment that
// *contains* a call, prefer the first inner call identifier instead of
// the whole line. Track the inner call's byte span so we can populate