Fix fn and bump frontend packages (#57)

* chore(deps): update frontend dependencies to latest versions

* fix: update reconnectTimer type and adjust tsconfig paths for consistency

* fix: add toast to dependencies in FindingsPage component

* fix: add toast to dependencies in FindingsPage component

* fix: update language maturity metrics and improve Go validation handling

* fix: update CHANGELOG with recent enhancements and dependency bumps

* fix: format reconnectTimer initialization for improved readability
This commit is contained in:
Eli Peter 2026-04-29 02:57:57 -04:00 committed by GitHub
parent 281699faae
commit 832533a8cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1210 additions and 1334 deletions

View file

@ -146,7 +146,10 @@ pub(super) fn extract_const_string_arg(
let mut cursor = args.walk();
let arg = args.named_children(&mut cursor).nth(index)?;
match arg.kind() {
"string" | "string_literal" => {
// `string` / `string_literal` cover JS/TS, Python, Java, PHP, C/C++, Ruby, Rust;
// `interpreted_string_literal` / `raw_string_literal` cover Go's
// tree-sitter grammar (double-quoted vs. backtick-quoted forms).
"string" | "string_literal" | "interpreted_string_literal" | "raw_string_literal" => {
let raw = text_of(arg, code)?;
if raw.len() >= 2 {
Some(raw[1..raw.len() - 1].to_string())
@ -906,8 +909,10 @@ pub(super) fn extract_kwargs(call_node: Node, code: &[u8]) -> Vec<(String, Vec<S
/// Policy is deliberately narrow and conservative: only literals that contain
/// *known-dangerous* payloads earn a strip credit, so an arbitrary
/// `.replace("foo", "bar")` is never promoted to a sanitizer.
/// * `..`, `/`, `\\` → path-traversal → `Cap::FILE_IO`
/// * `<`, `>` → HTML metachars → `Cap::HTML_ESCAPE`
/// * `..`, `/`, `\\` → path-traversal → `Cap::FILE_IO`
/// * `<`, `>` → HTML metachars → `Cap::HTML_ESCAPE`
/// * `;`, `|`, `&`, `$`, `\`` → shell metachars → `Cap::SHELL_ESCAPE`
/// * `'`, `"`, `--` → SQL metachars → `Cap::SQL_QUERY`
pub(super) fn caps_stripped_by_literal_pattern(search: &str) -> Cap {
let mut caps = Cap::empty();
if search.contains("..") || search.contains('/') || search.contains('\\') {
@ -916,6 +921,17 @@ pub(super) fn caps_stripped_by_literal_pattern(search: &str) -> Cap {
if search.contains('<') || search.contains('>') {
caps |= Cap::HTML_ESCAPE;
}
if search.contains(';')
|| search.contains('|')
|| search.contains('&')
|| search.contains('$')
|| search.contains('`')
{
caps |= Cap::SHELL_ESCAPE;
}
if search.contains('\'') || search.contains('"') || search.contains("--") {
caps |= Cap::SQL_QUERY;
}
caps
}
@ -1032,6 +1048,51 @@ pub(super) fn detect_rust_replace_chain_sanitizer(call_ast: Node, code: &[u8]) -
None
}
/// Recognise a Go `strings.Replace(s, OLD, NEW, n)` /
/// `strings.ReplaceAll(s, OLD, NEW)` call that provably strips one of the
/// known-dangerous metacharacter classes from its first argument.
///
/// Returns the union of caps stripped, or `None` when the pattern doesn't
/// apply (so the caller falls back to normal unresolved-call propagation).
///
/// Mirrors [`detect_rust_replace_chain_sanitizer`] but for the single-call
/// (non-method-chain) Go shape. The caller wires the resulting cap into
/// the call's [`crate::labels::DataLabel::Sanitizer`] label, which the
/// taint engine consumes via the standard sanitizer pathway — taint flows
/// in on `s`, the matching cap is stripped from the result.
pub(super) fn detect_go_replace_call_sanitizer(call_ast: Node, code: &[u8]) -> Option<Cap> {
if call_ast.kind() != "call_expression" {
return None;
}
// The call's `function` field is a `selector_expression` — `operand`
// is the package ident (`strings`), `field` is the method ident.
let func = call_ast.child_by_field_name("function")?;
if func.kind() != "selector_expression" {
return None;
}
let operand = func.child_by_field_name("operand")?;
if text_of(operand, code).as_deref() != Some("strings") {
return None;
}
let field = func.child_by_field_name("field")?;
let method_name = text_of(field, code)?;
if method_name != "Replace" && method_name != "ReplaceAll" {
return None;
}
// Args layout: (s, old, new[, n]). Need positional args 1 (old) and
// 2 (new) to be string literals.
let old_lit = extract_const_string_arg(call_ast, 1, code)?;
let new_lit = extract_const_string_arg(call_ast, 2, code)?;
// If the replacement itself reintroduces a dangerous sequence, don't
// credit the strip — matches the Rust chain detector's policy.
if !caps_stripped_by_literal_pattern(&new_lit).is_empty() {
return None;
}
let caps = caps_stripped_by_literal_pattern(&old_lit);
if caps.is_empty() { None } else { Some(caps) }
}
/// Like `first_call_ident`, but also checks if `n` itself is a call node.
/// `first_call_ident` only searches children, so when `n` IS the call
/// expression (e.g. the argument `sanitize(cmd)`), this function catches it.

View file

@ -49,12 +49,13 @@ use imports::{extract_import_bindings, extract_promisify_aliases};
#[cfg(test)]
use literals::has_sql_placeholders;
use literals::{
arg0_kind_and_interpolation, call_ident_of, def_use, detect_rust_replace_chain_sanitizer,
extract_arg_callees, extract_arg_string_literals, extract_arg_uses, extract_const_keyword_arg,
extract_const_string_arg, extract_destination_field_idents, extract_kwargs,
extract_literal_rhs, find_call_node, find_call_node_deep, find_chained_inner_call,
has_keyword_arg, has_only_literal_args, is_parameterized_query_call,
java_chain_arg0_kind_for_method, ruby_chain_arg0_for_method, walk_chain_inner_call_args,
arg0_kind_and_interpolation, call_ident_of, def_use, detect_go_replace_call_sanitizer,
detect_rust_replace_chain_sanitizer, extract_arg_callees, extract_arg_string_literals,
extract_arg_uses, extract_const_keyword_arg, extract_const_string_arg,
extract_destination_field_idents, extract_kwargs, extract_literal_rhs, find_call_node,
find_call_node_deep, find_chained_inner_call, has_keyword_arg, has_only_literal_args,
is_parameterized_query_call, java_chain_arg0_kind_for_method, ruby_chain_arg0_for_method,
walk_chain_inner_call_args,
};
use params::{
compute_container_and_kind, extract_param_meta, inject_framework_param_sources,
@ -1788,6 +1789,27 @@ pub(super) fn push_node<'a>(
}
}
// Pattern-based sanitizer synthesis for Go's `strings.Replace` /
// `strings.ReplaceAll`. When the call's OLD literal contains a known
// dangerous payload (shell metachars, path-traversal, HTML, SQL) and
// the NEW literal does not reintroduce one, treat the call as a
// Sanitizer over the matching caps. Same precedence as the Rust
// chain synthesis: explicit Sanitizer labels win, but otherwise the
// synthesised label feeds the standard sanitizer pathway in the
// taint engine. Motivated by helpers like
// `func validate(s string) string { return strings.ReplaceAll(s, ";", "") }`
// whose return is appended to a slice that later flows into
// `exec.Command(slice[i])`.
if lang == "go" && !labels.iter().any(|l| matches!(l, DataLabel::Sanitizer(_))) {
if let Some(cn) = call_ast {
if cn.kind() == "call_expression" {
if let Some(caps) = detect_go_replace_call_sanitizer(cn, code) {
labels.push(DataLabel::Sanitizer(caps));
}
}
}
}
// Shape-based sanitizer synthesis for Ruby ActiveRecord query methods.
// The static label table marks `where` / `order` / `pluck` / `group` /
// `having` / `joins` as `Sink(SQL_QUERY)` because their string-interpolation