mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
597 lines
21 KiB
Rust
597 lines
21 KiB
Rust
use crate::labels::{
|
|
Cap, DataLabel, GateActivation, Kind, LabelRule, ParamConfig, RuntimeLabelRule, SinkGate,
|
|
};
|
|
use crate::utils::project::{DetectedFramework, FrameworkContext};
|
|
use phf::{Map, phf_map};
|
|
|
|
pub static RULES: &[LabelRule] = &[
|
|
// ─────────── Sources ───────────
|
|
LabelRule {
|
|
matchers: &["os.Getenv", "os.LookupEnv", "os.Environ"],
|
|
label: DataLabel::Source(Cap::all()),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &[
|
|
"http.Request",
|
|
"r.FormValue",
|
|
"r.URL",
|
|
"r.Body",
|
|
"r.Header",
|
|
"r.Header.Get",
|
|
"r.Header.Values",
|
|
"r.URL.Query",
|
|
"r.URL.Query.Get",
|
|
"r.Cookie",
|
|
"r.Cookies",
|
|
"Request.FormValue",
|
|
"Request.URL",
|
|
],
|
|
label: DataLabel::Source(Cap::all()),
|
|
case_sensitive: false,
|
|
},
|
|
// ───────── Sanitizers ──────────
|
|
LabelRule {
|
|
matchers: &[
|
|
"html.EscapeString",
|
|
"template.HTMLEscapeString",
|
|
"template.HTMLEscaper",
|
|
],
|
|
label: DataLabel::Sanitizer(Cap::HTML_ESCAPE),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["url.QueryEscape", "url.PathEscape"],
|
|
label: DataLabel::Sanitizer(Cap::URL_ENCODE),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["filepath.Clean", "filepath.Base"],
|
|
label: DataLabel::Sanitizer(Cap::FILE_IO),
|
|
case_sensitive: false,
|
|
},
|
|
// Type conversion sanitizers
|
|
LabelRule {
|
|
matchers: &[
|
|
"strconv.Atoi",
|
|
"strconv.ParseInt",
|
|
"strconv.ParseFloat",
|
|
"strconv.ParseBool",
|
|
],
|
|
label: DataLabel::Sanitizer(Cap::all()),
|
|
case_sensitive: false,
|
|
},
|
|
// ─────────── Sinks ─────────────
|
|
LabelRule {
|
|
matchers: &["exec.Command"],
|
|
label: DataLabel::Sink(Cap::SHELL_ESCAPE),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &[
|
|
"db.Query",
|
|
"db.Exec",
|
|
"db.QueryRow",
|
|
"db.Prepare",
|
|
// goqu raw SQL literal builders: `goqu.L(s)` and the alias
|
|
// `goqu.Lit(s)` insert `s` verbatim into the generated SQL with no
|
|
// parameterisation. CVE-2026-41422 (daptin) loops a user-controlled
|
|
// `c.QueryArray("column")` value into `goqu.L(project)` to allow
|
|
// arbitrary SELECT subqueries. Modelled by name — `goqu.L` is the
|
|
// documented escape hatch for raw SQL. The safe siblings
|
|
// `goqu.I` (identifier), `goqu.C` (column), `goqu.T` (table),
|
|
// `goqu.V` (parameterised value), and the typed function
|
|
// constructors (`goqu.COUNT`, `goqu.SUM`, …) are not sinks.
|
|
"goqu.L",
|
|
"goqu.Lit",
|
|
],
|
|
label: DataLabel::Sink(Cap::SQL_QUERY),
|
|
case_sensitive: false,
|
|
},
|
|
// fmt.Printf/Sprintf write to stdout or build strings in memory, not
|
|
// security sinks. fmt.Fprintf writes to an io.Writer (often http.ResponseWriter)
|
|
// so it IS a security sink for XSS.
|
|
LabelRule {
|
|
matchers: &["fmt.Fprintf"],
|
|
label: DataLabel::Sink(Cap::HTML_ESCAPE),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &[
|
|
"os.Open",
|
|
"os.OpenFile",
|
|
"os.Create",
|
|
"ioutil.ReadFile",
|
|
"os.ReadFile",
|
|
// Mutating filesystem operations. Path-traversal CVEs commonly
|
|
// sink into delete/write rather than read (Owncast CVE-2024-31450
|
|
// sinks into `os.Remove(filepath.Join(root, userInput))`).
|
|
"os.Remove",
|
|
"os.RemoveAll",
|
|
"os.WriteFile",
|
|
"ioutil.WriteFile",
|
|
],
|
|
label: DataLabel::Sink(Cap::FILE_IO),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["template.HTML", "template.JS", "template.CSS"],
|
|
label: DataLabel::Sink(Cap::HTML_ESCAPE),
|
|
case_sensitive: false,
|
|
},
|
|
// ── Outbound HTTP clients (SSRF) ───────────────────────────────────
|
|
//
|
|
// These are modeled as destination-aware gated sinks in `GATED_SINKS`
|
|
// below. Flat Sink rules would over-flag every positional argument as
|
|
// SSRF (so a tainted body in `http.Post(url, contentType, body)` would
|
|
// fire SSRF on the body), and the gate machinery short-circuits when a
|
|
// flat Sink label is already attached to the callee, blocking DATA_EXFIL
|
|
// body-flow gates from running.
|
|
//
|
|
// `net.Dial` / `net.DialTimeout` keep their flat-sink modeling: the
|
|
// first positional arg is the network address with no body / payload
|
|
// companion, so the over-flag concern does not apply.
|
|
LabelRule {
|
|
matchers: &["net.Dial", "net.DialTimeout"],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &[
|
|
"md5.New",
|
|
"md5.Sum",
|
|
"sha1.New",
|
|
"sha1.Sum",
|
|
"des.NewCipher",
|
|
"rc4.NewCipher",
|
|
],
|
|
label: DataLabel::Sink(Cap::CRYPTO),
|
|
case_sensitive: false,
|
|
},
|
|
];
|
|
|
|
/// Argument-role-aware Go sinks. Two classes coexist on the outbound HTTP
|
|
/// surface, mirroring the JS/TS modeling:
|
|
///
|
|
/// * SSRF on the URL-bearing position of a one-shot request (`http.Get`,
|
|
/// `http.Post`, `http.NewRequest`, `http.DefaultClient.*`).
|
|
/// * `Cap::DATA_EXFIL` on the body / payload position when the source is
|
|
/// Sensitive (cookies, headers, env, db reads). Gates fire only when
|
|
/// taint reaches the body argument, so a tainted URL alone never
|
|
/// activates DATA_EXFIL and a tainted body alone never activates SSRF.
|
|
///
|
|
/// `http.NewRequest` / `http.NewRequestWithContext` carry an SSRF gate on
|
|
/// their URL position only. In Go's two-step idiom the actual network
|
|
/// call happens at `client.Do(req)`; body taint flows from the body
|
|
/// argument through the returned `*http.Request` via default arg → return
|
|
/// propagation, and then activates the `http.DefaultClient.Do` DATA_EXFIL
|
|
/// gate below. Modeling NewRequest as a body propagator (rather than a
|
|
/// body sink) avoids duplicate findings on the idiomatic
|
|
/// `req, _ := http.NewRequest(...); client.Do(req)` shape.
|
|
pub static GATED_SINKS: &[SinkGate] = &[
|
|
// ── SSRF gates (URL-bearing position) ────────────────────────────────
|
|
// `http.Get(url)` — url is arg 0.
|
|
SinkGate {
|
|
callee_matcher: "http.Get",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.Head(url)` — url is arg 0.
|
|
SinkGate {
|
|
callee_matcher: "http.Head",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.Post(url, contentType, body)` — url is arg 0.
|
|
SinkGate {
|
|
callee_matcher: "http.Post",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.PostForm(url, data)` — url is arg 0.
|
|
SinkGate {
|
|
callee_matcher: "http.PostForm",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.NewRequest(method, url, body)` — url is arg 1.
|
|
SinkGate {
|
|
callee_matcher: "http.NewRequest",
|
|
arg_index: 1,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[1],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.NewRequestWithContext(ctx, method, url, body)` — url is arg 2.
|
|
SinkGate {
|
|
callee_matcher: "http.NewRequestWithContext",
|
|
arg_index: 2,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[2],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.DefaultClient.Get(url)` / `.Head(url)` — url is arg 0.
|
|
SinkGate {
|
|
callee_matcher: "http.DefaultClient.Get",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
SinkGate {
|
|
callee_matcher: "http.DefaultClient.Head",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.DefaultClient.Post(url, contentType, body)` — url is arg 0.
|
|
SinkGate {
|
|
callee_matcher: "http.DefaultClient.Post",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.DefaultClient.PostForm(url, data)` — url is arg 0.
|
|
SinkGate {
|
|
callee_matcher: "http.DefaultClient.PostForm",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// ── DATA_EXFIL gates (body-bearing position) ─────────────────────────
|
|
// `http.Post(url, contentType, body)` — body is arg 2.
|
|
SinkGate {
|
|
callee_matcher: "http.Post",
|
|
arg_index: 2,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[2],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.PostForm(url, data)` — `data` (arg 1) is `url.Values`. Form
|
|
// bodies serialize the same operator state cookies / headers do, so a
|
|
// tainted Sensitive value reaching the form payload is DATA_EXFIL.
|
|
SinkGate {
|
|
callee_matcher: "http.PostForm",
|
|
arg_index: 1,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[1],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.DefaultClient.Do(req)` — `req` (arg 0) is the `*http.Request`
|
|
// value. Body taint introduced via either `http.NewRequest(_, _, body)`
|
|
// (default arg → return propagation) or a later `req.Body = body` field
|
|
// write reaches this sink through the request value.
|
|
SinkGate {
|
|
callee_matcher: "http.DefaultClient.Do",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.DefaultClient.PostForm(url, data)` — same as `http.PostForm`
|
|
// but invoked through the package-level default `*http.Client`.
|
|
SinkGate {
|
|
callee_matcher: "http.DefaultClient.PostForm",
|
|
arg_index: 1,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[1],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `http.DefaultClient.Post(url, contentType, body)` — body is arg 2.
|
|
SinkGate {
|
|
callee_matcher: "http.DefaultClient.Post",
|
|
arg_index: 2,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[2],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// ── Common third-party HTTP clients ─────────────────────────────────
|
|
//
|
|
// `go-resty/resty`: `client.R().SetBody(body).Post(url)` style.
|
|
// `SetBody(body)` carries the body into the chained request; the
|
|
// network call happens at the verb method. We model the verb
|
|
// methods (Get / Post / Put / Patch / Delete / Send / Execute) as
|
|
// DATA_EXFIL gates with `payload_args: &[]` (empty), which engages
|
|
// the receiver-tainted fallback in `collect_tainted_sink_vars`. A
|
|
// builder receiver carrying body taint from `SetBody` activates the
|
|
// sink without us needing a positional body arg.
|
|
SinkGate {
|
|
callee_matcher: "resty.Request.Post",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
SinkGate {
|
|
callee_matcher: "resty.Request.Put",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
SinkGate {
|
|
callee_matcher: "resty.Request.Patch",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// `imroc/req`: `req.Post(url, req.BodyJSON(payload))`, the `BodyJSON`
|
|
// / `BodyXML` helpers wrap a tainted payload and pass it as arg 1+ of
|
|
// the verb call. Since the helper return value carries the body
|
|
// taint, gating the verb on every payload arg is sufficient.
|
|
SinkGate {
|
|
callee_matcher: "req.Post",
|
|
arg_index: 1,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[1, 2, 3],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
SinkGate {
|
|
callee_matcher: "req.Put",
|
|
arg_index: 1,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: false,
|
|
payload_args: &[1, 2, 3],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
];
|
|
|
|
pub static KINDS: Map<&'static str, Kind> = phf_map! {
|
|
// control-flow
|
|
"if_statement" => Kind::If,
|
|
"for_statement" => Kind::For,
|
|
|
|
"return_statement" => Kind::Return,
|
|
"break_statement" => Kind::Break,
|
|
"continue_statement" => Kind::Continue,
|
|
|
|
// structure
|
|
"source_file" => Kind::SourceFile,
|
|
"block" => Kind::Block,
|
|
"statement_list" => Kind::Block,
|
|
"function_declaration" => Kind::Function,
|
|
"method_declaration" => Kind::Function,
|
|
"func_literal" => Kind::Function,
|
|
"expression_switch_statement" => Kind::Switch,
|
|
"type_switch_statement" => Kind::Switch,
|
|
"expression_case" => Kind::Block,
|
|
"type_case" => Kind::Block,
|
|
"default_case" => Kind::Block,
|
|
"select_statement" => Kind::Block,
|
|
"communication_case" => Kind::Block,
|
|
"go_statement" => Kind::Block,
|
|
"defer_statement" => Kind::Block,
|
|
|
|
// data-flow
|
|
"call_expression" => Kind::CallFn,
|
|
"assignment_statement" => Kind::Assignment,
|
|
"short_var_declaration" => Kind::CallWrapper,
|
|
"expression_statement" => Kind::CallWrapper,
|
|
"var_declaration" => Kind::CallWrapper,
|
|
"type_assertion_expression" => Kind::Seq,
|
|
|
|
// trivia
|
|
"comment" => Kind::Trivia,
|
|
";" => Kind::Trivia, "," => Kind::Trivia,
|
|
"(" => Kind::Trivia, ")" => Kind::Trivia,
|
|
"{" => Kind::Trivia, "}" => Kind::Trivia,
|
|
"\n" => Kind::Trivia,
|
|
"import_declaration" => Kind::Trivia,
|
|
"package_clause" => Kind::Trivia,
|
|
};
|
|
|
|
pub static PARAM_CONFIG: ParamConfig = ParamConfig {
|
|
params_field: "parameters",
|
|
param_node_kinds: &["parameter_declaration"],
|
|
self_param_kinds: &[],
|
|
ident_fields: &["name"],
|
|
};
|
|
|
|
/// Framework-conditional rules for Go.
|
|
pub fn framework_rules(ctx: &FrameworkContext) -> Vec<RuntimeLabelRule> {
|
|
let mut rules = Vec::new();
|
|
|
|
if ctx.has(DetectedFramework::Gin) {
|
|
rules.push(RuntimeLabelRule {
|
|
matchers: vec![
|
|
"c.Param".into(),
|
|
"c.Query".into(),
|
|
"c.PostForm".into(),
|
|
"c.DefaultQuery".into(),
|
|
"c.DefaultPostForm".into(),
|
|
"c.GetHeader".into(),
|
|
"c.Cookie".into(),
|
|
"c.BindJSON".into(),
|
|
"c.ShouldBindJSON".into(),
|
|
// Array-returning sibling helpers. `c.QueryArray("k")` returns
|
|
// every value of repeated query param `k`; `c.PostFormArray`
|
|
// and `c.GetQueryArray` / `c.GetPostFormArray` are the
|
|
// documented `[]string` counterparts of the scalar methods
|
|
// above. CVE-2026-41422 (daptin) reads `c.QueryArray("column")`
|
|
// and loops directly into a SQL_QUERY sink.
|
|
"c.QueryArray".into(),
|
|
"c.GetQueryArray".into(),
|
|
"c.PostFormArray".into(),
|
|
"c.GetPostFormArray".into(),
|
|
],
|
|
label: DataLabel::Source(Cap::all()),
|
|
case_sensitive: false,
|
|
});
|
|
rules.push(RuntimeLabelRule {
|
|
matchers: vec!["c.HTML".into(), "c.String".into()],
|
|
label: DataLabel::Sink(Cap::HTML_ESCAPE),
|
|
case_sensitive: false,
|
|
});
|
|
}
|
|
|
|
if ctx.has(DetectedFramework::Echo) {
|
|
rules.push(RuntimeLabelRule {
|
|
matchers: vec![
|
|
"c.QueryParam".into(),
|
|
"c.FormValue".into(),
|
|
"c.Param".into(),
|
|
"c.Bind".into(),
|
|
],
|
|
label: DataLabel::Source(Cap::all()),
|
|
case_sensitive: false,
|
|
});
|
|
rules.push(RuntimeLabelRule {
|
|
matchers: vec!["c.HTML".into(), "c.String".into(), "c.JSON".into()],
|
|
label: DataLabel::Sink(Cap::HTML_ESCAPE),
|
|
case_sensitive: false,
|
|
});
|
|
}
|
|
|
|
rules
|
|
}
|