Python fp and docs updtes (#58)

* refactor: Update comments for clarity and add expectations.json files for performance metrics

* feat: Implement FP guard for JS/TS local-collection receivers to suppress missing ownership checks

* feat: Enhance Rust parameter handling to classify local collections and prevent false ownership checks

* refactor: Simplify code formatting for better readability in multiple files

* refactor: Improve UTF-8 sequence length handling and enhance clarity in loop iteration

* feat: Update Java and Python patterns to include new security rules

* refactor: Improve comment clarity and consistency across multiple Rust files

* refactor: Simplify code formatting for improved readability in integration tests and module files

* refactor: Improve comment formatting and enhance clarity in assertions across multiple files
This commit is contained in:
Eli Peter 2026-04-29 19:53:34 -04:00 committed by GitHub
parent 4db0805de6
commit a438886217
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
291 changed files with 9485 additions and 3851 deletions

View file

@ -1,7 +1,7 @@
// Regression fixture: Rust async flow through `tokio::process::Command`.
//
// Per docs/language-maturity.md, Rust's Tokio process variants are not
// yet covered the Tokio async process APIs are a known gap. The
// yet covered, the Tokio async process APIs are a known gap. The
// fixture is checked in so that when Rust async-process coverage lands,
// the engine begins producing the intended finding and the
// `forbidden_findings` assertion forces whoever adds the coverage to

View file

@ -1,7 +1,7 @@
// Target: authorization happens inside `require_owner`, which
// delegates to `require_group_member` (a configured authorization
// check name). The handler in `cross_file_helper_handler.rs`
// delegates ownership validation to this helper cross-file helper
// delegates ownership validation to this helper, cross-file helper
// lifting should recognise the call as an auth check covering the
// supplied `row`.
struct Db;

View file

@ -2,7 +2,7 @@
// produces a `DatabaseConnection` via SSA `constructor_type` (through
// `peel_identity_suffix`, which strips `.unwrap()` before matching). The
// handler then calls `conn.execute(..)`, a callee name that appears in
// neither `mutation_indicator_names` nor `read_indicator_names` for Rust
// neither `mutation_indicator_names` nor `read_indicator_names` for Rust ,
// name-based classification returns `None`, so the ownership gate
// already cannot flag the call. The type-map refinement should *still*
// leave the call unflagged (the type map produces `DbMutation`, but

View file

@ -16,7 +16,7 @@ pub async fn handle_list_peer_docs(req: Req, ctx: Ctx) -> Result<String, ()> {
let user = auth::require_auth(&req, &ctx).await?;
let doc_ids: Vec<i64> = vec![1, 2, 3];
// Pure in-memory bookkeeping no authorization decision here.
// Pure in-memory bookkeeping, no authorization decision here.
let mut counts: HashMap<i64, usize> = HashMap::new();
let mut seen: HashSet<i64> = HashSet::new();
for doc_id in &doc_ids {

View file

@ -1,5 +1,5 @@
// B4 regression guard: `format_target` does NOT auth-check
// `group_id` it just constructs a string from it. The helper-lift
// `group_id`, it just constructs a string from it. The helper-lift
// pass must not synthesise a covering AuthCheck on the handler's call
// site, so the subsequent `db.exec("INSERT INTO comments …", &[group_id])`
// MUST still flag.
@ -19,7 +19,7 @@ mod auth {
}
fn format_target(group_id: i64, suffix: &str) -> String {
// No auth check here pure formatting.
// No auth check here, pure formatting.
format!("group:{}{}", group_id, suffix)
}

View file

@ -41,7 +41,7 @@ pub async fn handle_delete_doc(req: Req, ctx: Ctx, doc_id: i64) -> Result<String
return json_err("cannot delete another user's doc", 403);
}
// By construction, the row belongs to `user` so any id read from it is authorized.
// By construction, the row belongs to `user`, so any id read from it is authorized.
let group_id = existing.get_i64("group_id");
realtime::publish_to_group(group_id, "doc_deleted");
Ok("ok".into())

View file

@ -31,7 +31,7 @@ pub async fn handle_update_doc(req: Req, ctx: Ctx, doc_id: i64) -> Result<String
);
let owner_id = existing.get_i64("user_id");
// Equality compared but no early exit the check has no effect.
// Equality compared but no early exit, the check has no effect.
if owner_id != user.id {
// missing return
println!("not your doc (but proceeding anyway)");

View file

@ -5,7 +5,7 @@ mod auth { pub async fn require_auth(_r: &super::Req, _c: &super::Ctx) -> Result
// The handler's `get_peer_ids(&db, user.id)` call below must not be
// flagged. `user` is bound from `auth::require_auth(..)` so `user.id`
// is the caller's own id the call is self-referential, not a foreign
// is the caller's own id, the call is self-referential, not a foreign
// scoped id. The library-style helper below is a pass-through so its
// body contains no DB sinks (the internal `user_id` → DB flow is a
// separate pattern covered by helper-summary lifting).

View file

@ -2,7 +2,7 @@
// against an ACL table (`group_members`) with a WHERE clause that pins
// the row to the current user (`gm.user_id = ?1` bound to `user.id`).
// Every returned row is membership-gated by construction, so downstream
// uses of the row's columns (`group_id` here) are authorized the
// uses of the row's columns (`group_id` here) are authorized, the
// `realtime::publish_to_group` call MUST NOT be flagged as missing an
// ownership check after B3.
struct Ctx;

View file

@ -1,7 +1,7 @@
// B3 regression guard: the SELECT JOINs through `audit_log` (NOT in
// the configured ACL list) and the WHERE clause pins on
// `al.user_id = ?1`. The audit-log row's user is the audit subject,
// not the doc owner so this query does NOT prove caller ownership
// not the doc owner, so this query does NOT prove caller ownership
// of the returned `doc_id`. The downstream realtime publish MUST
// still flag for a missing ownership check after B3.
struct Ctx;

View file

@ -1,7 +1,7 @@
// target: authorization happens inside `validate_target`, which
// internally calls `authz::require_membership` against the same
// `group_id` the handler subsequently mutates. The current rule cannot
// see this transitively B4 lifts per-function auth-check summaries
// see this transitively, B4 lifts per-function auth-check summaries
// (which positional params are auth-checked) so the handler-level call
// to `validate_target(&db, group_id, user.id)` is recognised as an
// auth check covering `group_id`. Result: `db.exec(..)` MUST NOT flag
@ -45,7 +45,7 @@ pub async fn handle_create_comment(
let user = auth::require_auth(&req, &ctx).await?;
let db = Db;
// Authorization happens inside validate_target helper-summary
// Authorization happens inside validate_target, helper-summary
// lifting propagates the per-param auth check so this covers
// `group_id`.
validate_target(&db, group_id, user.id).await?;

View file

@ -0,0 +1,31 @@
// FP guard for `js.auth.missing_ownership_check` — JS built-in
// container receivers must not be classified as data-layer sinks.
// See `tests/benchmark/corpus/typescript/auth/safe_local_collection_receiver.ts`
// for the full real-repo distillation.
type ElementsMap = Map<string, { id: string }>;
function fromAlias(elementsMap: ElementsMap, id: string) {
return elementsMap.get(id);
}
function fromDirectGeneric(m: Map<string, string>, k: string) {
return m.get(k);
}
function fromArrayShorthand(arr: { id: string }[], targetId: string) {
return arr.find((x) => x.id === targetId);
}
function fromLocalConstructor() {
const cache = new Map<string, string>();
cache.set("a", "x");
return cache.get("a");
}
function fromSet(visited: Set<string>, k: string) {
if (!visited.has(k)) {
visited.add(k);
}
return visited.size;
}

View file

@ -0,0 +1,16 @@
{
"required_findings": [],
"forbidden_findings": [
{ "id_prefix": "js.auth.missing_ownership_check" }
],
"noise_budget": {
"max_total_findings": 1,
"max_high_findings": 0
},
"performance_expectations": {
"max_ms_no_index": 1000,
"max_ms_index_cold": 1500,
"max_ms_index_warm": 500,
"ci_mode": "lenient"
}
}

View file

@ -0,0 +1,16 @@
{
"required_findings": [],
"forbidden_findings": [
{ "id_prefix": "rs.auth.missing_ownership_check" }
],
"noise_budget": {
"max_total_findings": 2,
"max_high_findings": 0
},
"performance_expectations": {
"max_ms_no_index": 1000,
"max_ms_index_cold": 1500,
"max_ms_index_warm": 500,
"ci_mode": "lenient"
}
}

View file

@ -0,0 +1,93 @@
// Real-repo precision guard mirroring meilisearch's index-scheduler
// shape:
// crates/index-scheduler/src/scheduler/process_snapshot_creation.rs::remove_tasks
// (`unsafe fn remove_tasks(tasks: &[Task], dst: &std::path::Path,
// index_base_map_size: usize)` plus per-loop bitmap mutations on
// destructured heed `Database` handles), plus the LocalCollection
// receiver-type cluster
// (`crates/index-scheduler/src/scheduler/enterprise_edition/network.rs::balance_shards`,
// `unsharded: RoaringBitmap`).
//
// Both engine fixes must hold: the Rust `parameter` arm in
// `collect_param_names` (only descends into `pattern`, never `type`)
// and the Rust LocalCollection type-text classifier
// (`rust_type_to_local_collection`). Without either, this file would
// produce missing-ownership-check findings on internal helpers /
// in-memory bitmap mutations.
use std::collections::{BTreeSet, HashMap, HashSet};
struct RoaringBitmap;
impl RoaringBitmap {
fn new() -> Self { Self }
fn insert(&mut self, _x: u32) -> bool { true }
fn remove(&mut self, _x: u32) -> bool { true }
fn contains(&self, _x: u32) -> bool { true }
}
struct Task { uid: u32 }
struct Database;
impl Database {
fn delete(&self, _w: &mut u32, _u: &u32) -> Result<(), ()> { Ok(()) }
}
struct TaskQueue {
all_tasks: Database,
canceled_by: Database,
}
// Rust `parameter` arm: type-segment idents (`std`, `path`, `Path`)
// must NOT pollute `unit.params` and gate user-input-evidence open.
unsafe fn remove_tasks(
tasks: &[Task],
dst: &std::path::Path,
sz: usize,
) -> Result<(), ()> {
let _ = (dst, sz);
let mut wtxn = 0u32;
let task_queue = TaskQueue {
all_tasks: Database,
canceled_by: Database,
};
let TaskQueue { all_tasks, canceled_by } = task_queue;
for task in tasks {
all_tasks.delete(&mut wtxn, &task.uid)?;
canceled_by.delete(&mut wtxn, &task.uid)?;
}
Ok(())
}
// LocalCollection typed param: `unsharded: RoaringBitmap` resolves to
// `TypeKind::LocalCollection`, so `unsharded.insert(docid)` /
// `unsharded.remove(docid)` classify as `SinkClass::InMemoryLocal`
// (non-auth-relevant).
fn balance_shards(mut unsharded: RoaringBitmap, docid: u32) {
unsharded.insert(docid);
unsharded.remove(docid);
}
// `&'a mut HashMap<...>` reference + lifetime: ref-stripping must
// reach the type head.
fn store_shard_docids<'a>(
new_shard_docids: &'a mut HashMap<String, u32>,
shard: String,
docid: u32,
) {
new_shard_docids.insert(shard, docid);
}
fn add_user_id(ids: &mut HashSet<u64>, user_id: u64) {
ids.insert(user_id);
ids.remove(&user_id);
}
fn collect_seen(seen: &mut BTreeSet<u32>, item_id: u32) {
seen.insert(item_id);
}
fn build_local_set(task_id: u32) -> RoaringBitmap {
let mut s = RoaringBitmap::new();
s.insert(task_id);
s
}

View file

@ -0,0 +1,41 @@
// FP guard / panic guard — CFG condition-text truncation must be UTF-8 safe.
//
// Reproduces the gogs scan crash where a CodeMirror Gherkin tokenizer ships a
// long localised regex inside a boolean sub-condition (`stream.match(/.../) &&
// other`). When `push_condition_node` textualises the sub-expression, the
// regex literal exceeds MAX_CONDITION_TEXT_LEN (256 bytes); naive byte-slice
// truncation panicked when byte 256 landed inside a multi-byte UTF-8
// character (here Gurmukhi `ਖ`, three bytes). Engine fix in
// `src/utils/snippet.rs::truncate_at_char_boundary`, applied at three CFG
// sites + two symex display sites.
//
// Invariant: scanning this file must terminate without panicking the rayon
// worker, regardless of where byte 256 lands.
function tokenLocalisedFeatureKeyword(stream, state) {
if (
!state.inKeywordLine &&
state.allowFeature &&
stream.match(/(機能|功能|フィーチャ|기능|โครงหลัก|ความสามารถ|ความต้องการทางธุรกิจ|ಹೆಚ್ಚಳ|గుణము|ಮುಹಾಂದರಾ|ਮੁਹਾਂਦਰਾ|ਨਕਸ਼ ਨੁਹਾਰ|ਖਾਸੀਅਤ|रूप लेख|وِیژگی|خاصية|תכונה|Функціонал|Функция|Функционалност|Функционал|Үзенчәлеклелек|Свойство|Особина|Мөмкинлек|Могућност|Λειτουργία|Δυνατότητα|Właściwość|Vlastnosť|Trajto|Tính năng|Savybė|Požiadavka|Požadavek|Potrzeba biznesowa|Özellik|Osobina|Ominaisuus|Omadus|Mogućnost|Mogucnost|Jellemző|Funzionalità|Funktionalitéit|Funktionalität|Funkcja|Funkcionalnost|Funkcionalitāte|Funkcia|Fungsi|Functionaliteit|Funcționalitate|Funcţionalitate|Functionalitate|Funcionalitat|Funcionalidade|Fonctionnalité|Fitur|Fīča|Feature|Eiginleiki|Egenskap|Egenskab|Característica|Caracteristica|Business Need|Aspekt|Arwedd|Ability):/)
) {
state.inKeywordLine = true;
return "keyword";
}
return null;
}
// Sanity: also exercise the let-match-guard truncation site
// (`emit_rust_match_guard_if`) by way of a JS analogue with a CFG-relevant
// boolean chain that wraps localised text into the second branch. The CFG
// builder still has to textualise both arms.
function classify(s) {
if (
s.length > 0 &&
s.indexOf("ਨਕਸ਼ ਨੁਹਾਰ ਖਾਸੀਅਤ रूप लेख وِیژگی خاصية תכונה Функціонал Функция Функционалност Функционал Үзенчәлеклелек Свойство Особина Мөмкинлек Могућност Λειτουργία Δυνατότητα") >= 0
) {
return "localised";
}
return "ascii";
}
module.exports = { tokenLocalisedFeatureKeyword, classify };

View file

@ -0,0 +1,14 @@
{
"required_findings": [],
"forbidden_findings": [],
"noise_budget": {
"max_total_findings": 0,
"max_high_findings": 0
},
"performance_expectations": {
"max_ms_no_index": 1000,
"max_ms_index_cold": 1500,
"max_ms_index_warm": 500,
"ci_mode": "lenient"
}
}

View file

@ -0,0 +1,51 @@
"""
FP guard for FastAPI / Flask route-level dependency-injection auth.
The `dependencies=[Depends(requires_access_dag(...))]` decorator
authorises the entire handler every value the handler receives,
every row it fetches, and every operation downstream. The
`is_route_level` flag on the injected AuthCheck tells
`auth_check_covers_subject` to short-circuit `true`, suppressing
`py.auth.missing_ownership_check` on the body's ORM calls (`filter_by`,
`scalar`, ) and on row-variable receivers (`dag.cleanup_runs(...)`).
A bare route with no `dependencies=` keyword is a real ownership-
check FP the engine must still flag it. The vulnerable
counterpart lives in
`tests/benchmark/corpus/python/auth/vuln_fastapi_route_no_dependencies.py`.
"""
from fastapi import Depends, FastAPI
router = FastAPI()
def requires_access_dag(method: str, access_entity=None):
def check():
...
return check
@router.get(
"/{dag_id}/runs/{run_id}",
dependencies=[Depends(requires_access_dag(method="GET"))],
)
def get_dag_run(dag_id: str, run_id: str, session):
"""Path params + ORM call covered by route-level guard."""
dag_run = session.scalar(
select(DagRun).filter_by(dag_id=dag_id, run_id=run_id)
)
if dag_run is None:
raise HTTPException(404, "not found")
return dag_run
@router.delete(
"/{dag_id}",
dependencies=[Depends(requires_access_dag(method="DELETE"))],
)
def delete_dag(dag_id: str, session):
"""Row fetch + row-variable method call covered by route-level guard."""
dag = session.scalar(select(DagModel).where(DagModel.dag_id == dag_id))
if dag is None:
raise HTTPException(404, "not found")
dag.cleanup_runs(session=session)

View file

@ -0,0 +1,16 @@
{
"required_findings": [],
"forbidden_findings": [
{ "id_prefix": "py.auth.missing_ownership_check" }
],
"noise_budget": {
"max_total_findings": 2,
"max_high_findings": 0
},
"performance_expectations": {
"max_ms_no_index": 1500,
"max_ms_index_cold": 2000,
"max_ms_index_warm": 800,
"ci_mode": "lenient"
}
}

View file

@ -0,0 +1,40 @@
// Strapi-style ORM accessor: `<obj>.db.query(MODEL_UID).<orm_method>(...)`.
// MODEL_UID is a literal model identifier (not raw SQL); the trailing
// findOne/findMany/create/update/delete/count are intrinsically
// parameterised — the actual SQL is generated by the ORM and per-call
// values arrive through field-keyed object literals the driver escapes.
//
// FP-guard: cfg-unguarded-sink and taint-unsanitised-flow must NOT
// fire on this shape.
declare const strapi: any;
async function getApiToken(whereParams: Record<string, unknown>) {
return strapi.db.query('admin::api-token').findOne({
select: ['id', 'name', 'lastUsedAt'],
populate: ['permissions'],
where: whereParams,
});
}
async function listTokens() {
return strapi.db.query('admin::api-token').findMany({
where: { type: 'read-only' },
});
}
async function createToken(data: unknown) {
return strapi.db.query('admin::api-token').create({ data });
}
async function updateToken(id: number, data: unknown) {
return strapi.db.query('admin::api-token').update({ where: { id }, data });
}
async function deleteToken(id: number) {
return strapi.db.query('admin::api-token').delete({ where: { id } });
}
async function countTokens() {
return strapi.db.query('admin::api-token').count();
}

View file

@ -0,0 +1,17 @@
{
"required_findings": [],
"forbidden_findings": [
{ "id_prefix": "cfg-unguarded-sink" },
{ "id_prefix": "taint-unsanitised-flow" }
],
"noise_budget": {
"max_total_findings": 3,
"max_high_findings": 0
},
"performance_expectations": {
"max_ms_no_index": 1000,
"max_ms_index_cold": 1500,
"max_ms_index_warm": 500,
"ci_mode": "lenient"
}
}

View file

@ -0,0 +1,13 @@
// DATA_EXFIL fixture: a fixed destination URL and an attacker-influenced
// body. SSRF must NOT fire (destination is hardcoded) but `Cap::DATA_EXFIL`
// must fire on the body field — request-bound bytes are leaving the process
// via the outbound request payload.
//
// Driven by `fetch_data_exfil_integration_tests.rs`.
function leakBody(req) {
var payload = req.body.message;
fetch('/endpoint', {
method: 'POST',
body: payload,
});
}

View file

@ -0,0 +1,10 @@
// SSRF regression fixture: attacker-controlled destination URL. SSRF must
// fire on the URL flow (arg 0) and `Cap::DATA_EXFIL` must NOT fire — the two
// classes share the callee but cap attribution is per-position so a tainted
// URL never surfaces as data exfiltration.
//
// Driven by `fetch_data_exfil_integration_tests.rs`.
function proxy(req) {
var target = req.query.target;
fetch(target);
}

View file

@ -2,7 +2,7 @@ use std::env;
use std::fs;
use std::process::Command;
/// Infrastructure provisioning tool Rust core.
/// Infrastructure provisioning tool, Rust core.
/// Reads infrastructure config from environment and executes provisioning commands.
struct InfraConfig {
@ -56,7 +56,7 @@ fn apply_terraform() {
.unwrap();
}
/// Destroys infrastructure reads target from env.
/// Destroys infrastructure, reads target from env.
/// VULN: env var flows into Command
fn destroy_cluster() {
let cluster = env::var("DESTROY_TARGET").unwrap();

View file

@ -1,5 +1,9 @@
import java.sql.*;
import java.security.SecureRandom;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.apache.commons.text.StringSubstitutor;
class Negative {
// Safe: parameterized query
@ -19,4 +23,17 @@ class Negative {
void safeLiteralQuery(Statement stmt) throws Exception {
stmt.executeQuery("SELECT COUNT(*) FROM users");
}
// Safe: SnakeYAML 2.0 / explicit SafeConstructor CVE-2022-1471 fix shape.
void safeSnakeyamlSafeConstructor(String body) {
LoaderOptions opts = new LoaderOptions();
Yaml yaml = new Yaml(new SafeConstructor(opts));
Object data = yaml.load(body);
}
// Safe: empty StringSubstitutor no interpolator factory CVE-2022-42889 fix shape.
String safeStringSubstitutorPassthrough(String input) {
StringSubstitutor s = new StringSubstitutor();
return s.replace(input);
}
}

View file

@ -1,6 +1,8 @@
import java.io.*;
import java.util.Random;
import java.security.MessageDigest;
import org.yaml.snakeyaml.Yaml;
import org.apache.commons.text.StringSubstitutor;
class Positive {
// java.deser.readobject
@ -45,4 +47,16 @@ class Positive {
void triggerGetWriterPrint(javax.servlet.http.HttpServletResponse resp) throws Exception {
resp.getWriter().println("<html>" + "data" + "</html>");
}
// java.deser.snakeyaml_unsafe_constructor CVE-2022-1471 regression guard.
void triggerSnakeyamlUnsafeConstructor() throws Exception {
Yaml yaml = new Yaml();
Object data = yaml.load("payload");
}
// java.code_exec.text4shell_interpolator CVE-2022-42889 regression guard.
String triggerText4ShellInterpolator(String input) {
StringSubstitutor s = StringSubstitutor.createInterpolator();
return s.replace(input);
}
}

View file

@ -42,6 +42,14 @@ def trigger_yaml(data):
def trigger_sql_concat(cursor, user):
cursor.execute("SELECT * FROM users WHERE name = '" + user + "'")
# py.sqli.execute_format (f-string variant)
def trigger_sql_fstring(cursor, user):
cursor.execute(f"SELECT * FROM users WHERE name = '{user}'")
# py.sqli.text_format
def trigger_sqlalchemy_text_fstring(connection, user):
connection.execute(text(f"SELECT * FROM users WHERE name = '{user}'"))
# py.crypto.md5
def trigger_md5(data):
hashlib.md5(data)

View file

@ -23,7 +23,7 @@ mod auth {
// Negative control: the handler validates ownership via
// `authz::require_group_member(...)?` before the realtime publish. Phase C
// should NOT emit `rs.auth.missing_ownership_check.taint` here the
// should NOT emit `rs.auth.missing_ownership_check.taint` here, the
// sanitizer clears `UNAUTHORIZED_ID` from the argument SSA values.
pub async fn handle_publish_checked(Path(group_id): Path<i64>) -> Result<&'static str, ()> {
let user = auth::current_user();

View file

@ -1,6 +1,6 @@
{
"description": "fetch({url: taintedUrl, body: fixed}) — destination-aware object-literal case. url is tainted, must fire.",
"tags": ["taint", "ssrf", "fetch", "destination-aware", "object-config"],
"description": "fetch({url: taintedUrl, body: fixed}) — destination-aware object-literal case. url is tainted (SSRF), body is fixed. SSRF must fire and the cross-boundary data-exfiltration class (Cap::DATA_EXFIL) must NOT fire — the two classes share the callee but cap attribution is per-position.",
"tags": ["taint", "ssrf", "fetch", "destination-aware", "object-config", "cap-attribution"],
"modes": ["full"],
"expected": [
{
@ -10,6 +10,12 @@
"line_range": [6, 14],
"evidence_contains": [],
"notes": "req.query.target → fetch({url: target, ...}) — tainted destination field under object-literal shape."
},
{
"rule_id": "taint-data-exfiltration",
"must_not_match": true,
"line_range": [6, 14],
"notes": "body is a fixed literal '{}' — DATA_EXFIL must NOT fire on this site (regression guard for per-cap attribution)."
}
]
}

View file

@ -1,6 +1,6 @@
{
"description": "fetch() request body carries attacker-controlled content but the destination URL is fixed. Under the destination-aware SSRF gate, only taint reaching the URL (arg 0 / object `url` field) activates — body taint must be silenced.",
"tags": ["taint", "ssrf", "fetch", "destination-aware", "regression-fp"],
"description": "fetch() with a fixed destination URL and an attacker-controlled body. SSRF must NOT fire (destination is not attacker-influenced) and the cross-boundary data-exfiltration class (Cap::DATA_EXFIL) MUST fire on the body field.",
"tags": ["taint", "data-exfil", "fetch", "destination-aware", "cap-attribution"],
"modes": ["full"],
"expected": [
{
@ -8,6 +8,12 @@
"must_not_match": true,
"line_range": [7, 14],
"notes": "fetch('/api/telemetry', {body: payload}) — arg 0 is a fixed string, body taint must not fire as SSRF."
},
{
"rule_id": "taint-data-exfiltration",
"must_match": true,
"line_range": [7, 14],
"notes": "Body field carries req.body.message → must fire DATA_EXFIL (sensitive data leaving the process via outbound request payload)."
}
]
}

View file

@ -2,7 +2,7 @@ use std::env;
use std::fs;
// Wrapper whose replace chain strips only unrelated characters. The scanner
// must NOT treat this as a path-traversal sanitizer the taint path should
// must NOT treat this as a path-traversal sanitizer, the taint path should
// still be flagged.
fn rewrite(s: &str) -> String {
s.replace("foo", "bar").replace("baz", "qux")

View file

@ -1,5 +1,5 @@
// Session-module validate: accidentally shells out with its param.
// Same name + arity as auth::token::validate ambiguous without a use map.
// Same name + arity as auth::token::validate, ambiguous without a use map.
// If cross-file resolution incorrectly targets this function from main.rs,
// the param taint from env::var will flow into Command::arg → taint finding.
pub fn validate(input: &str) -> String {

View file

@ -1,5 +1,5 @@
// Token-module validate: strips shell metacharacters and returns a safe value.
// No sink in the body purely a pass-through sanitizer.
// No sink in the body, purely a pass-through sanitizer.
pub fn validate(input: &str) -> String {
input.replace(['&', ';', '|', '$', '`', '\\', '"', '\''], "")
}

View file

@ -1,7 +1,7 @@
use crate::auth::token::validate;
// `validate(&cmd)` must resolve unambiguously to `auth::token::validate`
// (a pass-through sanitizer) NOT `auth::session::validate` (which sinks
// (a pass-through sanitizer), NOT `auth::session::validate` (which sinks
// its arg into std::process::Command). A correct use-map driven resolver
// produces zero cross-file taint findings on this file.
fn main() {

View file

@ -1,6 +1,6 @@
use std::process::Command;
// #[inline] is NOT an auth attribute finding should fire.
// #[inline] is NOT an auth attribute, finding should fire.
#[inline]
fn handle_request(req: &str) {
Command::new("sh").arg("-c").arg("ls /tmp").status().unwrap();

View file

@ -1,5 +1,5 @@
fn boxed() {
let b = Box::new(42);
println!("{}", b);
// b dropped no leak
// b dropped, no leak
}

View file

@ -5,5 +5,5 @@ fn read_file() {
let mut f = File::open("/tmp/test").unwrap();
let mut buf = String::new();
f.read_to_string(&mut buf).unwrap();
// f dropped by RAII no leak
// f dropped by RAII, no leak
}

View file

@ -19,7 +19,7 @@ enum Cap {
pub fn dispatch(cap: Cap) {
let user_cmd = env::var("USER_CMD").unwrap_or_default();
match cap {
// Raw arm tainted user_cmd flows directly into the shell.
// Raw arm, tainted user_cmd flows directly into the shell.
Cap::Raw => {
Command::new("sh")
.arg("-c")
@ -27,7 +27,7 @@ pub fn dispatch(cap: Cap) {
.output()
.unwrap();
}
// Safe arm allowlist-guarded execution.
// Safe arm, allowlist-guarded execution.
Cap::Safe => {
let allowed = ["ls", "date"];
if allowed.contains(&user_cmd.as_str()) {