mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
Added Cap::DATA_EXFIL and taint fp and fn fixes on real repos (#59)
* feat: Enhance data exfiltration detection with source sensitivity gating for cookies and headers * feat: Implement cross-file data exfiltration detection with parameter-specific gate filters * feat: Add calibration tests and refine DATA_EXFIL severity scoring logic * feat: Introduce per-detector configuration for data exfiltration suppression * feat: Enhance DATA_EXFIL findings with destination field tracking in diagnostics and SARIF output * feat: Add tainted body and URL handling for data exfiltration detection * feat: Add integration tests and fixtures for DATA_EXFIL and SSRF detection in Go * feat: Add Java integration tests and fixtures for DATA_EXFIL detection across multiple HTTP clients * feat: Add synthetic externals handling for closure-captured variables in SSA * feat: Implement closure-based suppression for resource leak findings * feat: Add regression guards for shell-injection and taint propagation in for-of destructure patterns * feat: Implement constructor cap narrowing for data exfiltration detection in HTTP request builders * feat: Add gated sinks for data exfiltration detection in C and C++ using curl_easy_setopt * feat: Implement DATA_EXFIL cap parity for backwards analysis and add integration tests * feat: Add data exfiltration sinks for various languages and enhance documentation * refactor: Simplify formatting and improve readability in various files * refactor: Improve readability by simplifying conditional statements and adding clippy linting * docs: Update CHANGELOG and comments for data exfiltration features and configuration * docs: Clarify configuration instructions for data exfiltration trusted destinations * docs: Enhance comments for evidence routing logic in data exfiltration
This commit is contained in:
parent
a438886217
commit
58f1794a4e
189 changed files with 8421 additions and 383 deletions
|
|
@ -217,6 +217,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -638,6 +638,8 @@ mod tests {
|
|||
exception_edges: Vec::new(),
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -215,6 +215,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let (eliminated, copy_map) = copy_propagate(&mut body, &cfg);
|
||||
|
|
@ -296,6 +298,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let (eliminated, copy_map) = copy_propagate(&mut body, &cfg);
|
||||
|
|
@ -366,6 +370,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
(cfg, body)
|
||||
}
|
||||
|
|
@ -488,6 +494,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
let (eliminated, _map) = copy_propagate(&mut body, &cfg);
|
||||
assert_eq!(eliminated, 0, "two-operand Assign is not a copy");
|
||||
|
|
@ -567,6 +575,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
let (eliminated, _) = copy_propagate(&mut body, &cfg);
|
||||
assert_eq!(eliminated, 1, "v1 should be eliminated");
|
||||
|
|
@ -664,6 +674,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
let (eliminated, _map) = copy_propagate(&mut body, &cfg);
|
||||
assert_eq!(eliminated, 1);
|
||||
|
|
@ -712,6 +724,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
let (eliminated, map) = copy_propagate(&mut body, &cfg);
|
||||
assert_eq!(eliminated, 0);
|
||||
|
|
|
|||
|
|
@ -217,6 +217,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -265,6 +267,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -314,6 +318,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -359,6 +365,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -396,6 +404,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -460,6 +470,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: interner,
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -527,6 +539,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: interner,
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -587,6 +601,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -637,6 +653,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -724,6 +742,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
@ -801,6 +821,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let removed = eliminate_dead_defs(&mut body, &cfg);
|
||||
|
|
|
|||
|
|
@ -788,6 +788,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
let errs = check_structural_invariants(&body);
|
||||
assert!(
|
||||
|
|
@ -835,6 +837,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
let errs = check_structural_invariants(&body);
|
||||
assert!(
|
||||
|
|
@ -885,6 +889,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
let errs = check_structural_invariants(&body);
|
||||
assert!(
|
||||
|
|
@ -913,6 +919,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
let errs = check_structural_invariants(&body);
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::ssa::type_facts::TypeKind;
|
|||
use petgraph::graph::NodeIndex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Unique identifier for an SSA value (one per definition point).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
|
|
@ -353,6 +353,26 @@ pub struct SsaBody {
|
|||
/// cleanly with an empty map (no migration needed).
|
||||
#[serde(default)]
|
||||
pub field_writes: HashMap<SsaValue, (SsaValue, FieldId)>,
|
||||
/// SSA values that lowering injected for **free / closure-captured**
|
||||
/// variables (variables referenced by the body but not declared as
|
||||
/// formal parameters and not assigned within the body).
|
||||
///
|
||||
/// Lowering models every external use as an [`SsaOp::Param`] in block
|
||||
/// 0 so the rename pass can reference it. Real formal parameters and
|
||||
/// closure captures end up using the same op variant; this side-table
|
||||
/// distinguishes the two so downstream analyses (in particular the
|
||||
/// JS/TS handler-name auto-seed in
|
||||
/// [`crate::taint::ssa_transfer`]) can avoid treating closure
|
||||
/// captures as if they were the function's own parameters. Without
|
||||
/// this distinction, a lambda body that references an out-of-scope
|
||||
/// `userId` / `cmd` / `payload` would have the synthetic Param
|
||||
/// auto-seeded as `UserInput`, producing a phantom source on the
|
||||
/// enclosing function's declaration line.
|
||||
///
|
||||
/// `#[serde(default)]` for backward compatibility with summary blobs
|
||||
/// produced before this field existed.
|
||||
#[serde(default)]
|
||||
pub synthetic_externals: HashSet<SsaValue>,
|
||||
}
|
||||
|
||||
impl SsaBody {
|
||||
|
|
@ -560,6 +580,7 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: FieldInterner::new(),
|
||||
field_writes: HashMap::new(),
|
||||
synthetic_externals: HashSet::new(),
|
||||
};
|
||||
let fid = body.intern_field("mu");
|
||||
body.blocks[0].body.push(SsaInst {
|
||||
|
|
|
|||
|
|
@ -239,18 +239,25 @@ fn lower_to_ssa_inner(
|
|||
|
||||
// 6. Rename variables (dominator tree preorder walk)
|
||||
let dom_tree_children = build_dom_tree_children(num_blocks, &doms, &block_graph);
|
||||
let (mut ssa_blocks, mut value_defs, cfg_node_map, field_interner, field_writes) =
|
||||
rename_variables(
|
||||
cfg,
|
||||
&blocks_nodes,
|
||||
&block_succs,
|
||||
&block_preds,
|
||||
&phi_placements,
|
||||
&dom_tree_children,
|
||||
&filtered_edges,
|
||||
&external_vars,
|
||||
&nop_nodes,
|
||||
);
|
||||
let (
|
||||
mut ssa_blocks,
|
||||
mut value_defs,
|
||||
cfg_node_map,
|
||||
field_interner,
|
||||
field_writes,
|
||||
synthetic_externals,
|
||||
) = rename_variables(
|
||||
cfg,
|
||||
&blocks_nodes,
|
||||
&block_succs,
|
||||
&block_preds,
|
||||
&phi_placements,
|
||||
&dom_tree_children,
|
||||
&filtered_edges,
|
||||
&external_vars,
|
||||
formal_params,
|
||||
&nop_nodes,
|
||||
);
|
||||
|
||||
// 6b. Fill any missing phi operands with a shared Undef sentinel so
|
||||
// every phi has exactly one operand per predecessor. See
|
||||
|
|
@ -306,6 +313,7 @@ fn lower_to_ssa_inner(
|
|||
exception_edges,
|
||||
field_interner,
|
||||
field_writes,
|
||||
synthetic_externals,
|
||||
};
|
||||
|
||||
// 9. Catch-block reachability invariant.
|
||||
|
|
@ -927,6 +935,7 @@ fn rename_variables(
|
|||
dom_tree_children: &[Vec<usize>],
|
||||
filtered_edges: &[(NodeIndex, NodeIndex, EdgeKind)],
|
||||
external_vars: &[String],
|
||||
formal_params: &[String],
|
||||
nop_nodes: &HashSet<NodeIndex>,
|
||||
) -> (
|
||||
Vec<SsaBlock>,
|
||||
|
|
@ -934,6 +943,7 @@ fn rename_variables(
|
|||
HashMap<NodeIndex, SsaValue>,
|
||||
crate::ssa::ir::FieldInterner,
|
||||
HashMap<SsaValue, (SsaValue, crate::ssa::ir::FieldId)>,
|
||||
HashSet<SsaValue>,
|
||||
) {
|
||||
let num_blocks = blocks_nodes.len();
|
||||
let mut next_value: u32 = 0;
|
||||
|
|
@ -1679,6 +1689,27 @@ fn rename_variables(
|
|||
// Inject synthetic Param instructions at START of block 0 for external variables.
|
||||
// These create SSA definitions so the rename pass can reference them.
|
||||
// Pre-seed var_stacks so process_block sees them.
|
||||
//
|
||||
// `external_vars` contains both real formal parameters and free / closure-
|
||||
// captured variables (variables read by the body but not declared as a
|
||||
// formal and not assigned anywhere). Both end up emitted as
|
||||
// [`SsaOp::Param`] in block 0; we record the SSA values that correspond
|
||||
// to free vars in `synthetic_externals` so downstream analyses (the JS/TS
|
||||
// handler-name auto-seed in particular) can avoid treating closure
|
||||
// captures as if they were parameters of the function under analysis.
|
||||
//
|
||||
// **Conservative behaviour when `formal_params` is empty.** Several
|
||||
// call sites (`lower_to_ssa`, `lower_to_ssa_scoped_nop`) don't supply
|
||||
// formal parameter names; in that case we cannot distinguish formals
|
||||
// from free vars structurally, so we leave `synthetic_externals` empty
|
||||
// and the auto-seed pass keeps its pre-fix behaviour of treating every
|
||||
// `Param` op as a candidate. Only callers that pass a non-empty
|
||||
// `formal_params` slice (`lower_to_ssa_with_params`, used by the
|
||||
// findings pipeline's per-function lowering) opt into the
|
||||
// closure-capture distinction.
|
||||
let mut synthetic_externals: HashSet<SsaValue> = HashSet::new();
|
||||
let formal_set: HashSet<&str> = formal_params.iter().map(|s| s.as_str()).collect();
|
||||
let track_synthetic = !formal_params.is_empty();
|
||||
if !external_vars.is_empty() {
|
||||
let entry_cfg_node = blocks_nodes[0][0];
|
||||
let mut synthetic_body = Vec::with_capacity(external_vars.len());
|
||||
|
|
@ -1691,7 +1722,8 @@ fn rename_variables(
|
|||
cfg_node: entry_cfg_node,
|
||||
block: BlockId(0),
|
||||
});
|
||||
let op = if is_receiver_name(var) {
|
||||
let is_receiver = is_receiver_name(var);
|
||||
let op = if is_receiver {
|
||||
SsaOp::SelfParam
|
||||
} else {
|
||||
let op = SsaOp::Param {
|
||||
|
|
@ -1700,6 +1732,28 @@ fn rename_variables(
|
|||
positional_idx += 1;
|
||||
op
|
||||
};
|
||||
// A non-receiver var is "synthetic" (a free / closure capture)
|
||||
// when it is *not* one of the function's declared formals AND
|
||||
// not a dotted access on a formal (`input.cmd` where `input` is
|
||||
// a formal — it represents a structural projection of the
|
||||
// formal, not a free variable; the auto-seed should still treat
|
||||
// it as part of the formal's own taint surface). Receivers are
|
||||
// intentionally excluded: `this` / `self` represent the implicit
|
||||
// receiver, which always belongs to the function.
|
||||
//
|
||||
// Only fire when the caller supplied formal-parameter names; see
|
||||
// the `track_synthetic` rationale above.
|
||||
let root_is_formal = var
|
||||
.split_once('.')
|
||||
.map(|(root, _)| formal_set.contains(root))
|
||||
.unwrap_or(false);
|
||||
if track_synthetic
|
||||
&& !is_receiver
|
||||
&& !formal_set.contains(var.as_str())
|
||||
&& !root_is_formal
|
||||
{
|
||||
synthetic_externals.insert(v);
|
||||
}
|
||||
synthetic_body.push(SsaInst {
|
||||
value: v,
|
||||
op,
|
||||
|
|
@ -1784,6 +1838,7 @@ fn rename_variables(
|
|||
cfg_node_map,
|
||||
field_interner,
|
||||
field_writes,
|
||||
synthetic_externals,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -417,6 +417,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -440,6 +440,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
let cfg: Cfg = Graph::new();
|
||||
let const_values = HashMap::new();
|
||||
|
|
|
|||
|
|
@ -25,6 +25,15 @@ pub enum TypeKind {
|
|||
FileHandle,
|
||||
Url,
|
||||
HttpClient,
|
||||
/// A pre-network HTTP request builder produced by `Client::post(url)`,
|
||||
/// `surf::post(url)`, `Request::builder()`, `ureq::post(url)`, etc.
|
||||
/// The body-bind methods (`body`, `json`, `form`, `multipart`,
|
||||
/// `body_string`, `body_json`, `body_bytes`) and terminal verbs
|
||||
/// (`send`, `send_string`, `send_json`, `send_form`) are sinks for
|
||||
/// `DATA_EXFIL` when receiver-typed. Distinct from `HttpClient` so
|
||||
/// type-qualified resolution can attach builder-only rules without
|
||||
/// over-firing on plain client objects.
|
||||
RequestBuilder,
|
||||
/// A local, in-memory collection (HashMap, HashSet, Vec, etc.).
|
||||
/// The auth sink gate uses this so calls like `map.insert(...)`
|
||||
/// are treated as bookkeeping rather than cross-tenant sinks. No
|
||||
|
|
@ -76,6 +85,7 @@ impl TypeKind {
|
|||
Self::DatabaseConnection => Some("DatabaseConnection"),
|
||||
Self::FileHandle => Some("FileHandle"),
|
||||
Self::Url => Some("URL"),
|
||||
Self::RequestBuilder => Some("RequestBuilder"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -180,9 +190,10 @@ impl TypeFactResult {
|
|||
///
|
||||
/// Suppression policy:
|
||||
/// * [`TypeKind::Int`] (and float, treated as numeric): suppresses
|
||||
/// `SQL_QUERY`, `FILE_IO`, `SHELL_ESCAPE`, `HTML_ESCAPE`, `SSRF` ,
|
||||
/// numeric values cannot carry the metacharacters required to drive
|
||||
/// any of these injection classes.
|
||||
/// `SQL_QUERY`, `FILE_IO`, `SHELL_ESCAPE`, `HTML_ESCAPE`, `SSRF`,
|
||||
/// `DATA_EXFIL`, numeric values cannot carry the metacharacters
|
||||
/// required to drive any of these injection classes, nor can they
|
||||
/// encode credentials/tokens that meaningfully constitute leakage.
|
||||
/// * [`TypeKind::Bool`]: suppresses every type-suppressible bit ,
|
||||
/// `true`/`false` cannot carry a payload of any kind.
|
||||
pub fn is_type_safe_for_sink(
|
||||
|
|
@ -191,8 +202,12 @@ pub fn is_type_safe_for_sink(
|
|||
type_facts: &TypeFactResult,
|
||||
) -> bool {
|
||||
use crate::labels::Cap;
|
||||
let type_suppressible =
|
||||
Cap::SQL_QUERY | Cap::FILE_IO | Cap::SHELL_ESCAPE | Cap::HTML_ESCAPE | Cap::SSRF;
|
||||
let type_suppressible = Cap::SQL_QUERY
|
||||
| Cap::FILE_IO
|
||||
| Cap::SHELL_ESCAPE
|
||||
| Cap::HTML_ESCAPE
|
||||
| Cap::SSRF
|
||||
| Cap::DATA_EXFIL;
|
||||
if !sink_caps.intersects(type_suppressible) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -224,6 +239,13 @@ pub(crate) fn constructor_type(lang: Lang, callee: &str) -> Option<TypeKind> {
|
|||
"newHttpClient" | "newBuilder" if callee.contains("HttpClient") => {
|
||||
Some(TypeKind::HttpClient)
|
||||
}
|
||||
// Apache HttpClient idiomatic factory:
|
||||
// `CloseableHttpClient client = HttpClients.createDefault();`
|
||||
// `HttpClients` contains the substring `HttpClient` so this
|
||||
// doesn't widen to unrelated `createDefault` calls.
|
||||
"createDefault" | "custom" if callee.contains("HttpClient") => {
|
||||
Some(TypeKind::HttpClient)
|
||||
}
|
||||
"OkHttpClient" | "WebClient" | "RestTemplate" => Some(TypeKind::HttpClient),
|
||||
"getConnection" => Some(TypeKind::DatabaseConnection),
|
||||
"MongoClient" => Some(TypeKind::DatabaseConnection),
|
||||
|
|
@ -340,6 +362,10 @@ pub(crate) fn constructor_type(lang: Lang, callee: &str) -> Option<TypeKind> {
|
|||
// so the auth sink gate recognises
|
||||
// `let x = factory_fn(); x.insert(..)`.
|
||||
Some(TypeKind::LocalCollection)
|
||||
} else if is_rust_request_builder_constructor(base) {
|
||||
// HTTP request-builder constructors across reqwest, surf,
|
||||
// ureq, hyper. See [`is_rust_request_builder_constructor`].
|
||||
Some(TypeKind::RequestBuilder)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -449,6 +475,54 @@ fn is_rust_local_collection_constructor(base: &str) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
/// Does the peeled Rust callee correspond to a known HTTP request-builder
|
||||
/// constructor / factory? Covers:
|
||||
/// * surf free verbs (`surf::post`, `surf::get`, ...) ,
|
||||
/// * ureq free verbs (`ureq::post`, ...) ,
|
||||
/// * hyper `Request::builder` ,
|
||||
/// * reqwest `Client::post(url)` / `Client::get(url)` etc. (the `Client`
|
||||
/// instance is itself an `HttpClient` but the verb call on it returns a
|
||||
/// `RequestBuilder` whose chained methods bind body/json/form/etc.).
|
||||
///
|
||||
/// reqwest's `Client::new` keeps its existing `HttpClient` mapping ,
|
||||
/// it produces the client, not a builder.
|
||||
fn is_rust_request_builder_constructor(base: &str) -> bool {
|
||||
// surf free verbs that return Request (acts as a builder).
|
||||
const SURF_VERBS: &[&str] = &[
|
||||
"post", "get", "put", "delete", "patch", "head", "connect", "trace",
|
||||
];
|
||||
if SURF_VERBS
|
||||
.iter()
|
||||
.any(|v| base.ends_with(&format!("surf::{v}")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// ureq free verbs that return Request.
|
||||
const UREQ_VERBS: &[&str] = &["post", "get", "put", "delete", "patch", "head"];
|
||||
if UREQ_VERBS
|
||||
.iter()
|
||||
.any(|v| base.ends_with(&format!("ureq::{v}")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// hyper request builder.
|
||||
if base.ends_with("Request::builder") || base.ends_with("hyper::Request::builder") {
|
||||
return true;
|
||||
}
|
||||
// reqwest Client verb-on-instance. `Client::post(url)` /
|
||||
// `Client::get(url)` chained-form returns a RequestBuilder. We match
|
||||
// the constructor-style segment used by chain text after CFG receiver
|
||||
// collapse (`reqwest::Client::new.post`, `Client::post`, etc.).
|
||||
const REQWEST_CLIENT_VERBS: &[&str] =
|
||||
&["post", "get", "put", "delete", "patch", "head", "request"];
|
||||
if REQWEST_CLIENT_VERBS.iter().any(|v| {
|
||||
base.ends_with(&format!("Client::new.{v}")) || base.ends_with(&format!("Client::{v}"))
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_identity_method(callee: &str) -> bool {
|
||||
let suffix = callee.rsplit(['.', ':']).next().unwrap_or(callee);
|
||||
matches!(
|
||||
|
|
@ -1076,6 +1150,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let consts = HashMap::from([
|
||||
|
|
@ -1189,6 +1265,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let consts = HashMap::new();
|
||||
|
|
@ -1220,9 +1298,10 @@ mod tests {
|
|||
}
|
||||
|
||||
/// Int-typed values must suppress every type-suppressible
|
||||
/// cap, including the freshly-added `SSRF` bit. Numeric IDs
|
||||
/// cannot rewrite a URL host, cannot form path traversal sequences,
|
||||
/// cannot carry SQL/HTML/shell metacharacters.
|
||||
/// cap, including the freshly-added `SSRF` and `DATA_EXFIL` bits.
|
||||
/// Numeric IDs cannot rewrite a URL host, cannot form path
|
||||
/// traversal sequences, cannot carry SQL/HTML/shell metacharacters,
|
||||
/// and do not encode credentials worth exfiltrating.
|
||||
#[test]
|
||||
fn int_suppresses_every_type_suppressible_cap() {
|
||||
use crate::labels::Cap;
|
||||
|
|
@ -1236,6 +1315,7 @@ mod tests {
|
|||
Cap::SHELL_ESCAPE,
|
||||
Cap::HTML_ESCAPE,
|
||||
Cap::SSRF,
|
||||
Cap::DATA_EXFIL,
|
||||
] {
|
||||
assert!(
|
||||
is_type_safe_for_sink(&[SsaValue(0)], cap, &result),
|
||||
|
|
@ -1271,6 +1351,7 @@ mod tests {
|
|||
Cap::SHELL_ESCAPE,
|
||||
Cap::HTML_ESCAPE,
|
||||
Cap::SSRF,
|
||||
Cap::DATA_EXFIL,
|
||||
] {
|
||||
assert!(
|
||||
is_type_safe_for_sink(&[SsaValue(0)], cap, &result),
|
||||
|
|
@ -1307,14 +1388,14 @@ mod tests {
|
|||
/// `is_type_safe_for_sink` requires an intentional matrix edit + a
|
||||
/// test update. Truth values:
|
||||
///
|
||||
/// | TypeKind | SQL | FILE | SHELL | HTML | SSRF | CODE_EXEC | DESERIALIZE |
|
||||
/// |-----------|-----|------|-------|------|------|-----------|-------------|
|
||||
/// | Int | Y | Y | Y | Y | Y | N | N |
|
||||
/// | Bool | Y | Y | Y | Y | Y | N | N |
|
||||
/// | String | N | N | N | N | N | N | N |
|
||||
/// | Url | N | N | N | N | N | N | N |
|
||||
/// | Object | N | N | N | N | N | N | N |
|
||||
/// | Unknown | N | N | N | N | N | N | N |
|
||||
/// | TypeKind | SQL | FILE | SHELL | HTML | SSRF | DATA_EXFIL | CODE_EXEC | DESERIALIZE |
|
||||
/// |-----------|-----|------|-------|------|------|------------|-----------|-------------|
|
||||
/// | Int | Y | Y | Y | Y | Y | Y | N | N |
|
||||
/// | Bool | Y | Y | Y | Y | Y | Y | N | N |
|
||||
/// | String | N | N | N | N | N | N | N | N |
|
||||
/// | Url | N | N | N | N | N | N | N | N |
|
||||
/// | Object | N | N | N | N | N | N | N | N |
|
||||
/// | Unknown | N | N | N | N | N | N | N | N |
|
||||
#[test]
|
||||
fn type_kind_cap_suppression_matrix() {
|
||||
use crate::labels::Cap;
|
||||
|
|
@ -1324,40 +1405,41 @@ mod tests {
|
|||
("SHELL_ESCAPE", Cap::SHELL_ESCAPE),
|
||||
("HTML_ESCAPE", Cap::HTML_ESCAPE),
|
||||
("SSRF", Cap::SSRF),
|
||||
("DATA_EXFIL", Cap::DATA_EXFIL),
|
||||
("CODE_EXEC", Cap::CODE_EXEC),
|
||||
("DESERIALIZE", Cap::DESERIALIZE),
|
||||
];
|
||||
// (kind_name, kind, [suppress for each cap in `caps` order])
|
||||
let rows: &[(&str, TypeKind, [bool; 7])] = &[
|
||||
let rows: &[(&str, TypeKind, [bool; 8])] = &[
|
||||
(
|
||||
"Int",
|
||||
TypeKind::Int,
|
||||
[true, true, true, true, true, false, false],
|
||||
[true, true, true, true, true, true, false, false],
|
||||
),
|
||||
(
|
||||
"Bool",
|
||||
TypeKind::Bool,
|
||||
[true, true, true, true, true, false, false],
|
||||
[true, true, true, true, true, true, false, false],
|
||||
),
|
||||
(
|
||||
"String",
|
||||
TypeKind::String,
|
||||
[false, false, false, false, false, false, false],
|
||||
[false, false, false, false, false, false, false, false],
|
||||
),
|
||||
(
|
||||
"Url",
|
||||
TypeKind::Url,
|
||||
[false, false, false, false, false, false, false],
|
||||
[false, false, false, false, false, false, false, false],
|
||||
),
|
||||
(
|
||||
"Object",
|
||||
TypeKind::Object,
|
||||
[false, false, false, false, false, false, false],
|
||||
[false, false, false, false, false, false, false, false],
|
||||
),
|
||||
(
|
||||
"Unknown",
|
||||
TypeKind::Unknown,
|
||||
[false, false, false, false, false, false, false],
|
||||
[false, false, false, false, false, false, false, false],
|
||||
),
|
||||
];
|
||||
for (kind_name, kind, expected) in rows {
|
||||
|
|
@ -1389,6 +1471,7 @@ mod tests {
|
|||
Cap::SHELL_ESCAPE,
|
||||
Cap::HTML_ESCAPE,
|
||||
Cap::SSRF,
|
||||
Cap::DATA_EXFIL,
|
||||
Cap::CODE_EXEC,
|
||||
Cap::DESERIALIZE,
|
||||
] {
|
||||
|
|
@ -1487,6 +1570,8 @@ mod tests {
|
|||
exception_edges: vec![],
|
||||
field_interner: crate::ssa::ir::FieldInterner::default(),
|
||||
field_writes: std::collections::HashMap::new(),
|
||||
|
||||
synthetic_externals: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
let consts = HashMap::new();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue