mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
679 lines
28 KiB
Rust
679 lines
28 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 ───────────
|
|
// Note: PHP `$` prefix is stripped by collect_idents, so match without `$`.
|
|
LabelRule {
|
|
matchers: &[
|
|
"$_GET",
|
|
"_GET",
|
|
"$_POST",
|
|
"_POST",
|
|
"$_REQUEST",
|
|
"_REQUEST",
|
|
"$_COOKIE",
|
|
"_COOKIE",
|
|
"$_FILES",
|
|
"_FILES",
|
|
"$_SERVER",
|
|
"_SERVER",
|
|
"$_ENV",
|
|
"_ENV",
|
|
],
|
|
label: DataLabel::Source(Cap::all()),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["file_get_contents", "fread"],
|
|
label: DataLabel::Source(Cap::all()),
|
|
case_sensitive: false,
|
|
},
|
|
// ───────── Sanitizers ──────────
|
|
LabelRule {
|
|
matchers: &["htmlspecialchars", "htmlentities"],
|
|
label: DataLabel::Sanitizer(Cap::HTML_ESCAPE),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["escapeshellarg", "escapeshellcmd"],
|
|
label: DataLabel::Sanitizer(Cap::SHELL_ESCAPE),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["basename", "realpath"],
|
|
label: DataLabel::Sanitizer(Cap::FILE_IO),
|
|
case_sensitive: false,
|
|
},
|
|
// PDO parameterized queries. `prepareStatement` covers Drupal's
|
|
// Database\\Connection convention (and any PSR-style wrapper that
|
|
// uses the longer name); semantically identical to `prepare` —
|
|
// both return a statement object, the bind step ships values as
|
|
// out-of-band parameters, no concatenation occurs.
|
|
LabelRule {
|
|
matchers: &["prepare", "prepareStatement", "bindParam", "bindValue"],
|
|
label: DataLabel::Sanitizer(Cap::SQL_QUERY),
|
|
case_sensitive: false,
|
|
},
|
|
// Phase 15 — `mysqli_real_escape_string($conn, $s)` and
|
|
// `pg_escape_string($s)` apply driver-side escaping for legacy
|
|
// string-concat shapes. Treat as SQL_QUERY sanitizers so the
|
|
// value-replacement clears the cap on the call return.
|
|
// `addslashes` is intentionally excluded — it does NOT cover
|
|
// multibyte / charset-aware injection vectors.
|
|
LabelRule {
|
|
matchers: &[
|
|
"mysqli_real_escape_string",
|
|
"pg_escape_string",
|
|
"pg_escape_literal",
|
|
"pg_escape_identifier",
|
|
],
|
|
label: DataLabel::Sanitizer(Cap::SQL_QUERY),
|
|
case_sensitive: false,
|
|
},
|
|
// Type-check sanitizers
|
|
LabelRule {
|
|
matchers: &["intval", "floatval", "ctype_digit", "ctype_alpha"],
|
|
label: DataLabel::Sanitizer(Cap::all()),
|
|
case_sensitive: false,
|
|
},
|
|
// PHP input filtering
|
|
LabelRule {
|
|
matchers: &["filter_input", "filter_var"],
|
|
label: DataLabel::Sanitizer(Cap::all()),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["urlencode", "rawurlencode"],
|
|
label: DataLabel::Sanitizer(Cap::URL_ENCODE),
|
|
case_sensitive: false,
|
|
},
|
|
// ─────────── Sinks ─────────────
|
|
LabelRule {
|
|
matchers: &[
|
|
"system",
|
|
"exec",
|
|
"passthru",
|
|
"shell_exec",
|
|
"proc_open",
|
|
"popen",
|
|
],
|
|
label: DataLabel::Sink(Cap::SHELL_ESCAPE),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["eval", "assert"],
|
|
label: DataLabel::Sink(Cap::CODE_EXEC),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["include", "include_once", "require", "require_once"],
|
|
label: DataLabel::Sink(Cap::FILE_IO),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["unserialize"],
|
|
label: DataLabel::Sink(Cap::DESERIALIZE),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["move_uploaded_file", "copy", "file_put_contents", "fwrite"],
|
|
label: DataLabel::Sink(Cap::FILE_IO),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["echo", "print"],
|
|
label: DataLabel::Sink(Cap::HTML_ESCAPE),
|
|
case_sensitive: false,
|
|
},
|
|
LabelRule {
|
|
matchers: &["mysqli_query", "pg_query", "pg_execute", "query"],
|
|
label: DataLabel::Sink(Cap::SQL_QUERY),
|
|
case_sensitive: false,
|
|
},
|
|
// PDO and MySQLi OOP: exec/prepare+execute patterns.
|
|
LabelRule {
|
|
matchers: &[
|
|
"pdo.exec",
|
|
"pdo.query",
|
|
"mysqli.real_query",
|
|
"mysqli_real_query",
|
|
// Phase 15 — `PDOStatement::execute` (with no args) executes a
|
|
// prepared statement; when prepared from a tainted string the
|
|
// bind step does NOT prevent injection (the SQL was already
|
|
// built unsafely). The receiver-text suffix is `stmt.execute`.
|
|
// Distinct from the bare `execute` matcher (already on the
|
|
// generic SQL_QUERY rule via `query` matcher) because the
|
|
// OOP `$stmt->execute()` shape skips the SQL-string arg.
|
|
"stmt.execute",
|
|
],
|
|
label: DataLabel::Sink(Cap::SQL_QUERY),
|
|
case_sensitive: false,
|
|
},
|
|
// Phase 15 — Doctrine ORM raw-SQL passthrough APIs. Doctrine's
|
|
// `EntityManager::createQuery($dql)` accepts a DQL string;
|
|
// `createNativeQuery($sql, $rsm)` accepts a native SQL string;
|
|
// `getConnection()->executeQuery($sql)` /
|
|
// `getConnection()->executeStatement($sql)` are the low-level
|
|
// Connection passthroughs that route to the underlying driver
|
|
// verbatim. Suffix-matching covers both bound-receiver shapes
|
|
// (`$em->createQuery($dql)`) and the documentation-style
|
|
// class-qualified call form (`EntityManager.createQuery`).
|
|
LabelRule {
|
|
matchers: &[
|
|
"EntityManager.createQuery",
|
|
"EntityManager.createNativeQuery",
|
|
"createQuery",
|
|
"createNativeQuery",
|
|
"executeQuery",
|
|
"executeStatement",
|
|
],
|
|
label: DataLabel::Sink(Cap::SQL_QUERY),
|
|
case_sensitive: true,
|
|
},
|
|
// Laravel Eloquent: raw SQL methods.
|
|
// DB::raw() → scoped_call_expression, callee text "DB.raw".
|
|
// whereRaw/selectRaw/orderByRaw/havingRaw → member_call_expression on query builder.
|
|
LabelRule {
|
|
matchers: &["DB.raw", "whereRaw", "selectRaw", "orderByRaw", "havingRaw"],
|
|
label: DataLabel::Sink(Cap::SQL_QUERY),
|
|
case_sensitive: false,
|
|
},
|
|
// Phase 15 — Laravel raw-SQL execution facade methods. `DB::select`,
|
|
// `DB::statement`, `DB::insert`, `DB::update`, `DB::delete`,
|
|
// `DB::unprepared` all accept a literal SQL string; the
|
|
// `unprepared` form is the explicit no-bind escape hatch.
|
|
LabelRule {
|
|
matchers: &[
|
|
"DB.select",
|
|
"DB.statement",
|
|
"DB.insert",
|
|
"DB.update",
|
|
"DB.delete",
|
|
"DB.unprepared",
|
|
],
|
|
label: DataLabel::Sink(Cap::SQL_QUERY),
|
|
case_sensitive: true,
|
|
},
|
|
// NOTE: `file_get_contents` and `fopen` can fetch URLs (SSRF vector) and
|
|
// local files (LFI vector — `file://` scheme). As a Sink(SSRF) they only
|
|
// fire when the argument is tainted. `fopen` is the canonical low-level
|
|
// stream-opening API used by media-import / OEmbed / podcast pipelines
|
|
// (CVE-2026-33486 in roadiz/documents wraps `fopen($url, 'r')` in a
|
|
// public `DownloadedFile::fromUrl` static method that any authenticated
|
|
// backend caller can drive with attacker-controlled URLs).
|
|
LabelRule {
|
|
matchers: &["file_get_contents", "curl_exec", "fopen"],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: false,
|
|
},
|
|
// Phase 14 — `\GuzzleHttp\Client::request($method, $url, ...)` and the
|
|
// verb-shorthand methods `$client->get($url)` / `->head($url)` /
|
|
// `->options($url)`. The read-shaped verbs carry the URL at arg 0
|
|
// and have no body argument, so a flat SSRF sink is FP-safe. The
|
|
// body-bearing verbs (`post` / `put` / `patch`) live on the
|
|
// DATA_EXFIL list above; their URL-position SSRF is covered via
|
|
// `Client.request` (arg 1 is URL) below as a flat sink — Guzzle
|
|
// does not expose argument-role-aware metadata that would let the
|
|
// gate distinguish URL from body, but the source-sensitivity gate
|
|
// already silences plain `$_GET` / `$_POST` flows so the
|
|
// remaining FP surface is small.
|
|
LabelRule {
|
|
matchers: &[
|
|
"Client.get",
|
|
"Client.head",
|
|
"Client.options",
|
|
"Client.request",
|
|
"HttpClient.get",
|
|
"HttpClient.head",
|
|
"HttpClient.request",
|
|
"Http.get",
|
|
"Http.head",
|
|
],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: true,
|
|
},
|
|
// ── Cross-boundary data exfiltration ──────────────────────────────────
|
|
//
|
|
// Body-bearing outbound HTTP verb methods on the major PHP HTTP clients.
|
|
// Flat sinks here compose with the SSRF rule on `curl_exec` /
|
|
// `file_get_contents` via multi-label classification. The
|
|
// source-sensitivity gate in `effective_sink_caps` strips DATA_EXFIL
|
|
// when the contributing source is `Plain` (`$_GET`, `$_POST`, `$_REQUEST`),
|
|
// so this only fires for sensitive sources (cookies / sessions /
|
|
// server-side state / env / file / db reads).
|
|
//
|
|
// Covered clients:
|
|
// * `Guzzle\Client::post/put/patch` — guzzlehttp/guzzle
|
|
// matched by suffix on the verb method (chained `$client->post(...)`).
|
|
// * `Symfony\HttpClient::request` — symfony/http-client
|
|
// request($method, $url, ['body' => $payload, 'json' => $data, ...])
|
|
// * `Http::post` — Laravel HTTP facade (over Guzzle)
|
|
LabelRule {
|
|
matchers: &[
|
|
"Client.post",
|
|
"Client.put",
|
|
"Client.patch",
|
|
"Client.request",
|
|
"HttpClient.post",
|
|
"HttpClient.put",
|
|
"HttpClient.patch",
|
|
"HttpClient.request",
|
|
"Http.post",
|
|
"Http.put",
|
|
"Http.patch",
|
|
],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: true,
|
|
},
|
|
// ─── LDAP injection sinks ───
|
|
//
|
|
// PHP's procedural LDAP API: `ldap_search($ds, $base, $filter)`,
|
|
// `ldap_list($ds, $base, $filter)`, `ldap_read($ds, $base, $filter)`.
|
|
// The filter argument is the LDAP-injection vector when concatenated
|
|
// with attacker-controlled input.
|
|
LabelRule {
|
|
matchers: &["ldap_search", "ldap_list", "ldap_read"],
|
|
label: DataLabel::Sink(Cap::LDAP_INJECTION),
|
|
case_sensitive: false,
|
|
},
|
|
// ─── LDAP-filter sanitizer ───
|
|
//
|
|
// `ldap_escape($value, $ignore, LDAP_ESCAPE_FILTER)` applies RFC 4515
|
|
// escaping; treat any `ldap_escape` call as clearing the LDAP_INJECTION
|
|
// cap (the no-flag default also escapes filter metacharacters
|
|
// conservatively).
|
|
LabelRule {
|
|
matchers: &["ldap_escape"],
|
|
label: DataLabel::Sanitizer(Cap::LDAP_INJECTION),
|
|
case_sensitive: false,
|
|
},
|
|
// ─── XPath injection sinks ───
|
|
//
|
|
// `DOMXPath::query($expr, $ctx)` and `DOMXPath::evaluate($expr, $ctx)`
|
|
// accept the expression string as arg 0; concatenated user input there
|
|
// is the canonical PHP XPath-injection vector. `SimpleXMLElement::xpath`
|
|
// takes the same shape. Direct flat matchers cover the
|
|
// class-qualified call forms.
|
|
// Type-qualified rewrites: `$xp = new DOMXPath($doc)` tags `$xp` as
|
|
// `TypeKind::XPathClient`, so `$xp->query(...)` / `$xp->evaluate(...)`
|
|
// resolve to `XPathClient.query` / `XPathClient.evaluate`. Without
|
|
// the distinct TypeKind, bare `query` would match the SQL_QUERY sink.
|
|
LabelRule {
|
|
matchers: &[
|
|
"XPathClient.query",
|
|
"XPathClient.evaluate",
|
|
"DOMXPath::query",
|
|
"DOMXPath::evaluate",
|
|
"SimpleXMLElement::xpath",
|
|
],
|
|
label: DataLabel::Sink(Cap::XPATH_INJECTION),
|
|
case_sensitive: false,
|
|
},
|
|
// Bare `xpath` method: SimpleXMLElement instances expose `->xpath($expr)`
|
|
// and Symfony / DOMCrawler wrappers do the same. Suffix matching on
|
|
// `xpath` covers `$xml->xpath(...)` and similar bound-receiver shapes
|
|
// where the receiver type is not statically known. Case-sensitive to
|
|
// avoid collisions with the `XPath` capitalisation used by qualified
|
|
// names.
|
|
LabelRule {
|
|
matchers: &["xpath"],
|
|
label: DataLabel::Sink(Cap::XPATH_INJECTION),
|
|
case_sensitive: true,
|
|
},
|
|
// ─── XPath escape sanitizers ───
|
|
//
|
|
// No PHP standard library helper escapes XPath metacharacters; project-
|
|
// local `escape_xpath` / `xpath_escape` are the developer-named
|
|
// equivalents.
|
|
LabelRule {
|
|
matchers: &["escape_xpath", "xpath_escape"],
|
|
label: DataLabel::Sanitizer(Cap::XPATH_INJECTION),
|
|
case_sensitive: false,
|
|
},
|
|
// ─── Header / CRLF injection sinks ───
|
|
//
|
|
// PHP's `header($line)` writes a raw header line. Tainted strings
|
|
// without `\r\n` stripping let an attacker inject extra headers
|
|
// (response splitting); see GATED_SINKS for the corresponding
|
|
// OPEN_REDIRECT co-tag on `Location: ...` forms.
|
|
//
|
|
// The HEADER_INJECTION sink is intentionally implemented as a gate
|
|
// (not a flat rule) so the multi-gate SSA dispatch can co-emit it
|
|
// alongside the OPEN_REDIRECT gate on the same call site, producing
|
|
// separate findings for each cap with their canonical rule ids.
|
|
// ─── Header / CRLF sanitizers ───
|
|
LabelRule {
|
|
matchers: &["strip_crlf", "escape_header", "sanitize_header"],
|
|
label: DataLabel::Sanitizer(Cap::HEADER_INJECTION),
|
|
case_sensitive: false,
|
|
},
|
|
// ─── Open-redirect URL allowlist sanitizers ───
|
|
//
|
|
// Mirrors the JS/TS rule. Developer-named functions that allowlist
|
|
// / scheme-strip a redirect URL clear OPEN_REDIRECT taint before it
|
|
// reaches `header("Location: …")`. PHP also commonly uses
|
|
// `snake_case` variants.
|
|
LabelRule {
|
|
matchers: &[
|
|
"validateRedirectUrl",
|
|
"isSafeRedirect",
|
|
"stripScheme",
|
|
"validate_redirect_url",
|
|
"is_safe_redirect",
|
|
"strip_scheme",
|
|
"ensure_relative_url",
|
|
"ensureRelativeUrl",
|
|
"assert_relative_path",
|
|
"assertRelativePath",
|
|
"is_relative_url",
|
|
"isRelativeUrl",
|
|
],
|
|
label: DataLabel::Sanitizer(Cap::OPEN_REDIRECT),
|
|
case_sensitive: false,
|
|
},
|
|
// ─── SSTI sinks ───
|
|
//
|
|
// Twig `\Twig\Environment::createTemplate(string $template)` parses an
|
|
// arbitrary template source string at runtime; a tainted source yields
|
|
// SSTI when the resulting template is rendered. `Environment::render`
|
|
// / `Environment::load` take a *template name* (file lookup, not source)
|
|
// and are intentionally excluded. After PHP scope-resolution stripping
|
|
// the chain text covers both `$twig->createTemplate($src)` and
|
|
// `Twig\Environment::createTemplate(...)` shapes.
|
|
LabelRule {
|
|
matchers: &["Environment.createTemplate", "Twig.createTemplate"],
|
|
label: DataLabel::Sink(Cap::SSTI),
|
|
case_sensitive: true,
|
|
},
|
|
// ─── XXE sanitizers ───
|
|
//
|
|
// `libxml_disable_entity_loader(true)` (PHP <8) / `libxml_set_external_entity_loader($cb)`
|
|
// disable external-entity expansion process-wide. Treat their return
|
|
// value as XXE-cleared so config-style fixtures (`libxml_disable_entity_loader(true);
|
|
// simplexml_load_string($xml, ...)`) suppress the gate when the call is
|
|
// present in the same SSA scope. The flat-rule sanitizer is a coarse
|
|
// approximation, the real config-check pattern would track parser-instance
|
|
// hardening (deferred Layer 2).
|
|
LabelRule {
|
|
matchers: &[
|
|
"libxml_disable_entity_loader",
|
|
"libxml_set_external_entity_loader",
|
|
],
|
|
label: DataLabel::Sanitizer(Cap::XXE),
|
|
case_sensitive: false,
|
|
},
|
|
];
|
|
|
|
/// Gated sinks for PHP.
|
|
///
|
|
/// `curl_setopt($ch, CURLOPT_POSTFIELDS, $payload)` is the canonical
|
|
/// non-OO PHP HTTP-egress payload binding. The activation arg (index 1) is
|
|
/// a `define`d constant: `CURLOPT_POSTFIELDS` (and the byref-copying variant
|
|
/// `CURLOPT_COPYPOSTFIELDS`) carry the request body, while other CURLOPT_*
|
|
/// constants designate URL / auth / TLS / behaviour, none of which is
|
|
/// DATA_EXFIL-relevant. Gating on the constant identifier keeps the rule
|
|
/// from over-firing on `curl_setopt($ch, CURLOPT_URL, $url)` (covered
|
|
/// elsewhere by the `curl_exec` SSRF flat sink).
|
|
///
|
|
/// Identifier-based activation is enabled via the macro-arg fallback in
|
|
/// `cfg::mod::classify_gated_sink` for `lang == "php"`.
|
|
pub static GATED_SINKS: &[SinkGate] = &[
|
|
SinkGate {
|
|
callee_matcher: "curl_setopt",
|
|
arg_index: 1,
|
|
dangerous_values: &["CURLOPT_POSTFIELDS", "CURLOPT_COPYPOSTFIELDS"],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::DATA_EXFIL),
|
|
case_sensitive: true,
|
|
payload_args: &[2],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::ValueMatch,
|
|
},
|
|
// Phase 14 — `curl_setopt($ch, CURLOPT_URL, $url)` is the canonical
|
|
// pre-`curl_exec` URL bind. Tainted `$url` reaching this option is
|
|
// SSRF; the `curl_exec($ch)` flat sink above also fires on the
|
|
// tainted handle but only when the handle's taint propagates
|
|
// through opaque resource state, which the engine cannot follow
|
|
// across `curl_setopt` calls. Activating the SSRF cap directly at
|
|
// the option-bind site catches the flow at the construction step
|
|
// independent of the handle-flow analysis.
|
|
SinkGate {
|
|
callee_matcher: "curl_setopt",
|
|
arg_index: 1,
|
|
dangerous_values: &["CURLOPT_URL"],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSRF),
|
|
case_sensitive: true,
|
|
payload_args: &[2],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::ValueMatch,
|
|
},
|
|
// PHP `header($line)` HEADER_INJECTION sink. Modelled as a gate so
|
|
// it can coexist with the OPEN_REDIRECT gate below: the multi-gate
|
|
// SSA dispatch needs each capability declared on its own gate filter
|
|
// to emit one finding per cap. Always activates (Destination), with
|
|
// payload arg 0 only (`header()` only accepts the line as arg 0;
|
|
// arg 1 is `replace`/`response_code`, not the line content).
|
|
SinkGate {
|
|
callee_matcher: "=header",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::HEADER_INJECTION),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::Destination {
|
|
object_destination_fields: &[],
|
|
},
|
|
},
|
|
// PHP `simplexml_load_string($xml, $class, $options)` —
|
|
// XXE sink gated on the `LIBXML_NOENT` flag (or `LIBXML_DTDLOAD`,
|
|
// `LIBXML_DTDATTR`). PHP's libxml is XXE-safe by default since 2.9.0;
|
|
// the gate fires only when the `$options` literal includes one of the
|
|
// dangerous flags. Identifier-based activation works via the macro-arg
|
|
// fallback in `cfg::mod::classify_gated_sink` for `lang == "php"`.
|
|
SinkGate {
|
|
callee_matcher: "simplexml_load_string",
|
|
arg_index: 2,
|
|
dangerous_values: &["LIBXML_NOENT", "LIBXML_DTDLOAD", "LIBXML_DTDATTR"],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::XXE),
|
|
case_sensitive: true,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::ValueMatch,
|
|
},
|
|
SinkGate {
|
|
callee_matcher: "simplexml_load_file",
|
|
arg_index: 2,
|
|
dangerous_values: &["LIBXML_NOENT", "LIBXML_DTDLOAD", "LIBXML_DTDATTR"],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::XXE),
|
|
case_sensitive: true,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::ValueMatch,
|
|
},
|
|
// DOMDocument::loadXML($xml, $options) — same gating as
|
|
// simplexml_load_string. The chain-normalised callee text for
|
|
// `$dom->loadXML(...)` is `dom.loadXML`; suffix matching on
|
|
// `loadXML` covers the bound-receiver form.
|
|
SinkGate {
|
|
callee_matcher: "loadXML",
|
|
arg_index: 1,
|
|
dangerous_values: &["LIBXML_NOENT", "LIBXML_DTDLOAD", "LIBXML_DTDATTR"],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::XXE),
|
|
case_sensitive: true,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::ValueMatch,
|
|
},
|
|
// PHP `header($line)` co-tag for OPEN_REDIRECT.
|
|
//
|
|
// The flat HEADER_INJECTION sink (`=header`) above already fires for
|
|
// any `header(...)` call regardless of the line content. This gate
|
|
// adds the OPEN_REDIRECT co-tag specifically when the first argument
|
|
// is a `Location: ...` header, so the dashboard / OWASP bucket
|
|
// correctly classifies redirect-class flows independently of CRLF.
|
|
//
|
|
// Activation: arg 0 prefix `Location:` (case-insensitive). When arg
|
|
// 0 is a constant string starting with `Location:` the gate fires and
|
|
// checks payload arg 0 for taint; constants like `Content-Type: ...`
|
|
// are suppressed by the safe-literal branch. When arg 0 is a binary
|
|
// expression (`"Location: " . $url`) or otherwise dynamic, the
|
|
// value-extraction returns `None` and the gate fires conservatively
|
|
// — matching the existing convention in `setAttribute`/`parseFromString`.
|
|
SinkGate {
|
|
callee_matcher: "=header",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &["Location:"],
|
|
label: DataLabel::Sink(Cap::OPEN_REDIRECT),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::ValueMatch,
|
|
},
|
|
// Smarty `$smarty->fetch($name)` — only the `string:` resource prefix
|
|
// accepts an inline template *source*; the bare form (`page.tpl`) is a
|
|
// file lookup (not SSTI). Gate activates only when arg 0's leading
|
|
// literal segment is the `string:` prefix; the constant-string suffix
|
|
// and concat (`"string:" . $src`) shapes both reach `extract_const_string_arg`'s
|
|
// leading-literal path and trigger activation. Payload is arg 0
|
|
// itself — taint reaching the template source string is the SSTI flow.
|
|
// Suffix matching catches both `Smarty.fetch` and the bound-receiver
|
|
// `$smarty->fetch(...)` forms.
|
|
SinkGate {
|
|
callee_matcher: "Smarty.fetch",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &["string:"],
|
|
label: DataLabel::Sink(Cap::SSTI),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
keyword_name: None,
|
|
dangerous_kwargs: &[],
|
|
activation: GateActivation::ValueMatch,
|
|
},
|
|
// Twig `\Twig\Environment::createTemplate(string $template)` —
|
|
// gated SSTI sink. Activation is unconditional (no value gate);
|
|
// payload arg 0 is the template source string. Bare suffix
|
|
// `createTemplate` matches the idiomatic instance shape
|
|
// `$twig->createTemplate($src)` (chain text `twig.createTemplate`)
|
|
// as well as the static `Environment::createTemplate(...)` form;
|
|
// `createTemplate` is Twig-specific terminology so over-fire risk
|
|
// is low. The matching flat rule remains for documentation-style
|
|
// class-qualified call shapes.
|
|
SinkGate {
|
|
callee_matcher: "createTemplate",
|
|
arg_index: 0,
|
|
dangerous_values: &[],
|
|
dangerous_prefixes: &[],
|
|
label: DataLabel::Sink(Cap::SSTI),
|
|
case_sensitive: false,
|
|
payload_args: &[0],
|
|
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,
|
|
"while_statement" => Kind::While,
|
|
"for_statement" => Kind::For,
|
|
"foreach_statement" => Kind::For,
|
|
"do_statement" => Kind::While,
|
|
|
|
"return_statement" => Kind::Return,
|
|
"throw_expression" => Kind::Throw,
|
|
"break_statement" => Kind::Break,
|
|
"continue_statement" => Kind::Continue,
|
|
|
|
// structure
|
|
"program" => Kind::SourceFile,
|
|
"compound_statement" => Kind::Block,
|
|
"else_clause" => Kind::Block,
|
|
"else_if_clause" => Kind::Block,
|
|
"function_definition" => Kind::Function,
|
|
"method_declaration" => Kind::Function,
|
|
"switch_statement" => Kind::Switch,
|
|
"switch_block" => Kind::Block,
|
|
"case_statement" => Kind::Block,
|
|
"default_statement" => Kind::Block,
|
|
"try_statement" => Kind::Try,
|
|
"catch_clause" => Kind::Block,
|
|
"finally_clause" => Kind::Block,
|
|
"colon_block" => Kind::Block,
|
|
"anonymous_function_creation_expression" => Kind::Function,
|
|
"arrow_function" => Kind::Function,
|
|
"class_declaration" => Kind::Block,
|
|
"declaration_list" => Kind::Block,
|
|
"interface_declaration" => Kind::Block,
|
|
"trait_declaration" => Kind::Block,
|
|
"enum_declaration" => Kind::Block,
|
|
"enum_declaration_list" => Kind::Block,
|
|
|
|
// data-flow
|
|
"function_call_expression" => Kind::CallFn,
|
|
"object_creation_expression" => Kind::CallFn,
|
|
"member_call_expression" => Kind::CallMethod,
|
|
"scoped_call_expression" => Kind::CallMethod,
|
|
"assignment_expression" => Kind::Assignment,
|
|
"expression_statement" => Kind::CallWrapper,
|
|
"echo_statement" => Kind::CallWrapper,
|
|
|
|
// trivia
|
|
"comment" => Kind::Trivia,
|
|
";" => Kind::Trivia, "," => Kind::Trivia,
|
|
"(" => Kind::Trivia, ")" => Kind::Trivia,
|
|
"{" => Kind::Trivia, "}" => Kind::Trivia,
|
|
"\n" => Kind::Trivia,
|
|
"php_tag" => Kind::Trivia,
|
|
"namespace_definition" => Kind::Trivia,
|
|
"namespace_use_declaration" => Kind::Trivia,
|
|
};
|
|
|
|
pub static PARAM_CONFIG: ParamConfig = ParamConfig {
|
|
params_field: "parameters",
|
|
param_node_kinds: &["simple_parameter", "variadic_parameter"],
|
|
self_param_kinds: &[],
|
|
ident_fields: &["name"],
|
|
};
|
|
|
|
/// Framework-conditional rules for PHP.
|
|
pub fn framework_rules(ctx: &FrameworkContext) -> Vec<RuntimeLabelRule> {
|
|
let mut rules = Vec::new();
|
|
|
|
if ctx.has(DetectedFramework::Laravel) {
|
|
rules.push(RuntimeLabelRule {
|
|
matchers: vec![
|
|
"Request::input".into(),
|
|
"Request::get".into(),
|
|
"Request::query".into(),
|
|
"Request::post".into(),
|
|
"Request::all".into(),
|
|
],
|
|
label: DataLabel::Source(Cap::all()),
|
|
case_sensitive: false,
|
|
});
|
|
}
|
|
|
|
rules
|
|
}
|