autoscan / docs /tasks.md
Chris4K's picture
Upload 384 files
a2a5bfd verified
|
Raw
History Blame Contribute Delete
35.9 kB
# autoscan β€” Copilot Task Instructions
**Project:** Chris4K/autoscan
**Purpose:** These are precise implementation tasks for GitHub Copilot.
Each task is self-contained: file to create, exact function signatures,
inputs/outputs, edge cases, and how it connects to the existing architecture.
Read `ARCHITECTURE.md` and `HOW_TO_EXTEND.md` before starting any task.
All scanner modules follow the pattern in `scanners/radon_runner.py` (see that file as template).
---
## TASK 01 β€” CVE Trigger Correlator
**File:** `scanners/cve_trigger_runner.py`
**Category:** `ml-security` | **Group flag:** `run_security=True`
**Binary required:** No β€” pure Python grep
**Tool name (for frozenset):** `cve-trigger`
### What it does
pip-audit already finds CVEs. This scanner cross-references each CVE found
against reachable trigger patterns in the Python source. A CVE without a
reachable trigger = skip. CVE + trigger in same repo = confirmed finding
with severity CRITICAL.
### Function signature
```python
def cve_trigger(work: str, pip_audit_findings: list[dict] | None = None) -> tuple[list[dict], str]:
```
`pip_audit_findings` is the output list from `pip_audit()` in the same scan.
If `None`, re-run pip-audit internally to get CVEs.
### CVE β†’ trigger mapping (hardcode this dict in the module)
```python
CVE_TRIGGERS: dict[str, list[str]] = {
# PyTorch
"PYSEC-2026-139": ["torch.load("], # pt2 deserialization RCE
"PYSEC-2025-194": ["torch.jit.script("], # JIT memory corruption RCE
"PYSEC-2025-195": ["torch.lstm_cell("], # lstm_cell RCE
"PYSEC-2025-192": ["pack_padded_sequence("], # RNN heap corruption
"PYSEC-2025-193": ["unpack_sequence("], # RNN heap corruption
"PYSEC-2025-210": ["torch.profiler.profile("], # profiler DoS
"PYSEC-2025-191": ["mkldnn_max_pool2d("], # pool DoS
"PYSEC-2025-197": ["caching_allocator_delete("], # CUDA allocator DoS
"PYSEC-2026-89": ["markdown.markdown(", "Markdown("], # markdown DoS
# Transformers
"PYSEC-2025-211": ["from_pretrained("], # Perceiver deserialization
"PYSEC-2025-212": ["from_pretrained("], # Transformer-XL deserialization
"PYSEC-2025-213": ["from_pretrained("], # megatron_gpt2 deserialization
"PYSEC-2025-214": ["convert_config("], # SEW code injection
"PYSEC-2025-215": ["convert_config("], # SEW-D code injection
"PYSEC-2025-216": ["convert_config("], # HuBERT code injection
"PYSEC-2025-217": ["from_pretrained("], # X-CLIP deserialization
"PYSEC-2025-218": ["from_pretrained("], # GLM4 deserialization
# CVE-2026-1839: Transformers Trainer torch.load without weights_only
"CVE-2026-1839": ["torch.load(", "_load_rng_state("],
# CVE-2025-32434: torch.load weights_only=True still unsafe before torch 2.6
"CVE-2025-32434": ["torch.load("],
# Keras CVE-2025-1550: custom layer RCE
"CVE-2025-1550": ["keras.models.load_model(", "tf.keras.models.load_model("],
# numpy
"PYSEC-numpy-001": ["np.load(", "numpy.load("], # allow_pickle
# joblib/sklearn
"PYSEC-joblib-001":["joblib.load("], # pickle RCE
}
```
### Trigger validation rules
For each trigger hit, apply these secondary checks before emitting a finding:
| Trigger | Extra condition required | Reason |
|---------|-------------------------|--------|
| `torch.load(` | Line must NOT contain `weights_only=True` | weights_only=True is safe |
| `np.load(` | Line must contain `allow_pickle=True` | default is safe since numpy 1.16.3 |
| `from_pretrained(` | Argument must NOT be a string literal | hardcoded model names are acceptable |
| `markdown.markdown(` | Argument must NOT be a string literal | static content is safe |
### Output per finding
```python
make_finding(
tool="cve-trigger",
rule=cve_id, # e.g. "PYSEC-2026-139"
severity="ERROR",
file=rel_path,
line=line_number,
message=f"{cve_id} trigger found: `{trigger}` β€” {description}. "
f"CVE confirmed exploitable if user input reaches this call.",
owasp=["A06:2021-Vulnerable_and_Outdated_Components"],
category="ml-security",
confidence="confirmed", # both CVE and trigger present = confirmed
)
```
### Edge cases
- If pip-audit not available and no `pip_audit_findings` passed β†’ return `([], "cve-trigger: pip-audit output unavailable")`
- If CVE found in pip-audit but no trigger match in code β†’ do NOT emit finding (this is the core value: filter noise)
- Same line may match multiple CVEs β†’ emit one finding per CVE, not per trigger
- Binary files β†’ skip silently
### Registration
- `scanners/__init__.py`: add `from .cve_trigger_runner import cve_trigger`
- `core/scanner.py` `_TASK_TO_TOOL`: `"cve-trigger": "cve-trigger"`
- `core/scanner.py` task list: append after pip_audit task, pass its results
- `sentinel/routes/discover.py` `_ALLOWED_SCANNERS`: add `"cve-trigger"`
- `sentinel/services/scanner.py` `_TOOL_NAMES`: add `"cve-trigger"` to `_sec_tools`
- `report/remediation.py`: add entries for each CVE ID
---
## TASK 02 β€” Gradio Global State Leak Detector
**File:** `scanners/gradio_state_runner.py`
**Category:** `ml-security` | **Group flag:** `run_security=True`
**Binary required:** No β€” pure Python AST
**Tool name:** `gradio-state`
### What it does
Parses all Python files with `ast`. Finds calls to `gr.State()` or
`gradio.State()` that appear at module scope (outside any function or
class method). These create shared global state visible to ALL users.
### Function signature
```python
def gradio_state(work: str) -> tuple[list[dict], str]:
```
### Detection logic
```python
import ast, pathlib
class StateVisitor(ast.NodeVisitor):
def __init__(self):
self.depth = 0 # function/class nesting depth
self.findings = []
def visit_FunctionDef(self, node):
self.depth += 1
self.generic_visit(node)
self.depth -= 1
visit_AsyncFunctionDef = visit_FunctionDef
def visit_ClassDef(self, node):
self.depth += 1
self.generic_visit(node)
self.depth -= 1
def visit_Call(self, node):
if self.depth == 0: # module scope only
name = _call_name(node) # "gr.State" or "gradio.State"
if name in ("gr.State", "gradio.State"):
self.findings.append(node.lineno)
self.generic_visit(node)
```
### Severity escalation
After finding global state, check the same file (and all files in `work`) for:
- `gr.ChatInterface` or `gr.Chatbot` β†’ escalate to `ERROR` / `confirmed`
(full LLM conversation history leaks between users)
- `openai`, `anthropic`, `requests.post` β†’ escalate to `ERROR`
(API calls may include prior user context)
- Otherwise β†’ `WARNING` / `likely`
### Output per finding
```python
make_finding(
tool="gradio-state",
rule="GRADIO-GLOBAL-STATE",
severity="ERROR" if has_llm else "WARNING",
file=rel_path,
line=line_no,
message="gr.State() defined at module scope β€” shared across ALL concurrent users. "
+ ("LLM chat history will leak between sessions." if has_llm else
"Any data stored here is visible to all users."),
owasp=["A01:2021-Broken_Access_Control"],
category="ml-security",
confidence="confirmed" if has_llm else "likely",
)
```
### Registration
Same pattern as Task 01. Tool name: `"gradio-state"`. Add to `_sec_tools`.
---
## TASK 03 β€” Semgrep Rule Pack: ML Model Loading
**File:** `rules/ml_pretrained.yaml`
**Registration:** Add to `ALL_SECURITY` in `rules/__init__.py`
**Label for semgrep_pack call:** `"Semgrep:ML-Pretrained"`
### Rules to implement
**Rule 1: trust-remote-code-user-input**
```yaml
- id: trust-remote-code-user-input
patterns:
- pattern: $M.from_pretrained($X, ..., trust_remote_code=True, ...)
- pattern-not: $M.from_pretrained("...", ..., trust_remote_code=True, ...)
message: >
trust_remote_code=True with a non-literal model name executes arbitrary Python
from the model repository. If $X is user-controlled this is unauthenticated RCE.
Use a hardcoded model name or validate against an allowlist.
severity: ERROR
languages: [python]
metadata:
owasp: ["A03:2021-Injection"]
confidence: confirmed
category: ml-security
cve: "PYSEC-2025-211,PYSEC-2025-218"
```
**Rule 2: torch-load-missing-weights-only**
```yaml
- id: torch-load-missing-weights-only
patterns:
- pattern: torch.load($X, ...)
- pattern-not: torch.load($X, ..., weights_only=True, ...)
- pattern-not: torch.load("...", ...)
message: >
torch.load() without weights_only=True deserializes via pickle β€” RCE if the
file is attacker-controlled. Add weights_only=True or switch to safetensors.
severity: ERROR
languages: [python]
metadata:
owasp: ["A08:2021-Software_and_Data_Integrity_Failures"]
confidence: likely
category: ml-security
cve: "PYSEC-2026-139,CVE-2025-32434,CVE-2026-1839"
```
**Rule 3: load-dataset-user-input**
```yaml
- id: load-dataset-user-input
patterns:
- pattern: load_dataset($X, ...)
- pattern-not: load_dataset("...", ...)
message: >
load_dataset() with a variable name downloads and executes the dataset's
Python loading script. If $X is user-controlled this is RCE without any
file upload. Pin to a hardcoded dataset name.
severity: ERROR
languages: [python]
metadata:
owasp: ["A03:2021-Injection"]
confidence: likely
category: ml-security
```
**Rule 4: joblib-load-user-input**
```yaml
- id: joblib-load-user-input
patterns:
- pattern: joblib.load($X)
- pattern-not: joblib.load("...")
message: >
joblib.load() uses pickle internally. Loading a user-supplied file path
executes arbitrary code. Validate the path against an allowlist.
severity: ERROR
languages: [python]
metadata:
owasp: ["A08:2021-Software_and_Data_Integrity_Failures"]
confidence: likely
category: ml-security
```
**Rule 5: numpy-allow-pickle**
```yaml
- id: numpy-allow-pickle-user-input
patterns:
- pattern: np.load($X, ..., allow_pickle=True, ...)
- pattern-not: np.load("...", ..., allow_pickle=True, ...)
message: >
numpy.load() with allow_pickle=True on a user-supplied path enables pickle RCE.
Use allow_pickle=False (default since numpy 1.16.3) or validate the file source.
severity: ERROR
languages: [python]
metadata:
owasp: ["A08:2021-Software_and_Data_Integrity_Failures"]
confidence: confirmed
category: ml-security
```
**Rule 6: safetensors-metadata-eval**
```yaml
- id: safetensors-metadata-eval
patterns:
- pattern: |
with safe_open($F, ...) as $H:
...
$X = $H.metadata()
...
eval($X[...])
- pattern: |
with safe_open($F, ...) as $H:
...
$M = $H.metadata()
...
importlib.import_module($M[...])
message: >
safetensors metadata is attacker-controlled when files come from untrusted sources.
eval() or importlib on metadata values enables code injection even in the
"safe" safetensors format.
severity: ERROR
languages: [python]
metadata:
owasp: ["A03:2021-Injection"]
confidence: confirmed
category: ml-security
```
**Rule 7: keras-load-model-user-input**
```yaml
- id: keras-load-model-user-input
patterns:
- pattern: keras.models.load_model($X)
- pattern-not: keras.models.load_model("...")
message: >
keras.models.load_model() on a user-supplied path is RCE via custom Lambda layers
(CVE-2025-1550). Use safe_mode=True (Keras 3+) or validate the source.
severity: ERROR
languages: [python]
metadata:
owasp: ["A08:2021-Software_and_Data_Integrity_Failures"]
confidence: likely
category: ml-security
cve: "CVE-2025-1550"
```
---
## TASK 04 β€” Semgrep Rule Pack: Gradio DoS / API Abuse
**File:** `rules/ml_gradio_dos.yaml`
**Registration:** Add to `ALL_SECURITY` in `rules/__init__.py`
**Label:** `"Semgrep:ML-GradioDoS"`
### Rules to implement
**Rule 1: gradio-unbounded-numeric-input**
```yaml
- id: gradio-unbounded-numeric-input
patterns:
- pattern: gr.Number(label=$L, ...)
- pattern-not: gr.Number(..., maximum=..., ...)
- metavariable-regex:
metavariable: $L
regex: '(?i)(limit|count|size|num|top_k|batch|steps|iter|max|n_result)'
message: >
gr.Number('$L') has no maximum= bound. Sending limit=999999 via the /run/predict
API (bypassing the UI slider) can exhaust memory or hammer downstream APIs.
Add maximum=<reasonable_cap> to the component definition.
severity: WARNING
languages: [python]
metadata:
owasp: ["A05:2021-Security_Misconfiguration"]
confidence: likely
category: ml-security
```
**Rule 2: gradio-slider-no-maximum**
```yaml
- id: gradio-slider-no-maximum
patterns:
- pattern: gr.Slider(label=$L, ...)
- pattern-not: gr.Slider(..., maximum=..., ...)
- metavariable-regex:
metavariable: $L
regex: '(?i)(limit|count|size|num|token|step)'
message: >
gr.Slider('$L') without maximum= β€” API callers can exceed the visual slider range.
severity: WARNING
languages: [python]
metadata:
owasp: ["A05:2021-Security_Misconfiguration"]
confidence: possible
category: ml-security
```
**Rule 3: gradio-concurrency-one-blocks-all**
```yaml
- id: gradio-concurrency-limit-one
patterns:
- pattern: $FN.click(..., concurrency_limit=1, ...)
message: >
concurrency_limit=1 means a single long-running request (e.g. max_tokens=99999)
blocks all other users. Pair with input length validation.
severity: WARNING
languages: [python]
metadata:
owasp: ["A05:2021-Security_Misconfiguration"]
confidence: possible
category: ml-security
```
---
## TASK 05 β€” Semgrep Rule Pack: Gradio SSRF + OAuth Token Leak
**File:** `rules/ml_gradio_ssrf.yaml`
**Registration:** Add to `ALL_SECURITY`
**Label:** `"Semgrep:ML-GradioSSRF"`
### Rules to implement
**Rule 1: gr-load-ssrf (CVE-2026-28416)**
```yaml
- id: gr-load-ssrf
patterns:
- pattern: gr.load($X, ...)
- pattern-not: gr.load("...", ...)
message: >
gr.load() with a variable Space name makes an HTTP request from HF infrastructure
to the supplied URL (CVE-2026-28416). If $X is user-controlled this is SSRF.
Use a hardcoded Space name or validate against an allowlist.
severity: ERROR
languages: [python]
metadata:
owasp: ["A10:2021-Server_Side_Request_Forgery"]
confidence: likely
category: ml-security
cve: "CVE-2026-28416"
```
**Rule 2: gradio-mocked-oauth-token-leak (CVE-2026-27167)**
```yaml
- id: gradio-mocked-oauth-token-leak
patterns:
- pattern: gr.LoginButton(...)
message: >
gr.LoginButton() outside HF Spaces enables a mocked OAuth route at
/login/huggingface that returns the server owner's HF access token to
any visitor (CVE-2026-27167, fixed in Gradio 6.6.0).
Ensure gradio>=6.6.0 or remove gr.LoginButton if running outside HF Spaces.
severity: ERROR
languages: [python]
metadata:
owasp: ["A02:2021-Cryptographic_Failures"]
confidence: likely
category: ml-security
cve: "CVE-2026-27167"
```
**Rule 3: gradio-no-revision-pin**
```yaml
- id: from-pretrained-no-revision-pin
patterns:
- pattern: $M.from_pretrained($X, ...)
- pattern-not: $M.from_pretrained($X, ..., revision=..., ...)
message: >
from_pretrained() without revision= always pulls HEAD. If the upstream
repo is compromised and pushes new code, the next Space restart silently
executes it (Bandit B615). Pin to a commit hash: revision="abc123".
severity: WARNING
languages: [python]
metadata:
owasp: ["A08:2021-Software_and_Data_Integrity_Failures"]
confidence: possible
category: ml-security
```
---
## TASK 06 β€” Semgrep Rule Pack: MCP Security
**File:** `rules/ml_mcp.yaml`
**Registration:** Add to `ALL_LLM` in `rules/__init__.py`
**Label:** `"Semgrep:ML-MCP"`
### Rules to implement
**Rule 1: mcp-server-no-auth**
```yaml
- id: mcp-server-no-auth
patterns:
- pattern: $D.launch(..., mcp_server=True, ...)
- pattern-not: $D.launch(..., auth=..., ...)
message: >
mcp_server=True exposes ALL Gradio functions as MCP tools with no
authentication. Any MCP client can invoke every tool without credentials.
Add auth= or restrict the exposed functions explicitly.
severity: WARNING
languages: [python]
metadata:
owasp: ["A01:2021-Broken_Access_Control"]
confidence: confirmed
category: llm
```
**Rule 2: mcp-dynamic-docstring-injection**
```yaml
- id: mcp-dynamic-docstring
pattern: $F.__doc__ = $X
message: >
Dynamically assigned __doc__ on a function pollutes the MCP tool schema.
If $X is user-influenced, attackers can inject instructions into the tool
description read by LLM clients (prompt injection via tool metadata).
severity: WARNING
languages: [python]
metadata:
owasp: ["A03:2021-Injection"]
confidence: possible
category: llm
```
**Rule 3: mcp-tool-no-input-validation**
```yaml
- id: mcp-tool-string-no-validation
patterns:
- pattern: |
def $F($X: str, ...):
...
$API($X, ...)
- pattern-not: |
def $F($X: str, ...):
...
if ...:
...
$API($X, ...)
message: >
MCP tool function '$F' accepts a string parameter and passes it directly
to an API call without input validation. MCP clients (LLMs) can pass
malicious values. Add validation before the API call.
severity: WARNING
languages: [python]
metadata:
owasp: ["A03:2021-Injection"]
confidence: possible
category: llm
```
---
## TASK 07 β€” README Injection Scanner
**File:** `scanners/readme_inject_runner.py`
**Category:** `llm` | **Group flag:** `run_llm=True`
**Binary required:** No β€” pure Python regex
**Tool name:** `readme-inject`
### What it does
Scans `README.md`, `README.rst`, `README.txt` for patterns that look like
LLM prompt injection attempts. When an AI assistant summarizes a Space,
it reads the README β€” injected instructions execute in the LLM's context.
### Function signature
```python
def readme_inject(work: str) -> tuple[list[dict], str]:
```
### Detection patterns (compile all as `re.IGNORECASE | re.DOTALL`)
```python
PATTERNS = [
# Direct instruction injection
(r"SYSTEM\s*:", "Direct system prompt injection attempt"),
(r"IGNORE\s+(PREVIOUS|ALL|PRIOR)\s+INSTRUCTIONS?", "Classic jailbreak pattern"),
(r"YOU\s+ARE\s+NOW\s+IN\s+\w+\s+MODE", "Mode-switching injection"),
(r"\bOVERRIDE\b.{0,50}\bINSTRUCTIONS?\b", "Override injection"),
# Template injection markers
(r"<\|system\|>", "ChatML system token injection"),
(r"\[INST\]\s*<<SYS>>", "Llama instruction injection"),
(r"<\|im_start\|>system", "OpenAI chat format injection"),
(r"\{%.*?%\}", "Jinja2/template injection"),
# Hidden in HTML comments
(r"<!--.*?(SYSTEM|IGNORE|OVERRIDE|INJECT|assistant\s*:).*?-->",
"Instruction hidden in HTML comment"),
# Data exfiltration patterns
(r"fetch\s*\(\s*['\"]https?://(?!huggingface\.co)", "Exfil URL in README"),
(r"navigator\.sendBeacon", "JS beacon exfil attempt"),
# Encoding tricks
(r"&#x[0-9a-fA-F]{2,4};.*SYSTEM", "HTML-encoded injection"),
]
```
### Severity logic
- HTML comment injection β†’ `ERROR` (deliberate concealment)
- Exfil URL β†’ `ERROR`
- Direct system patterns β†’ `WARNING`
- Template markers β†’ `WARNING`
### Output
```python
make_finding(
tool="readme-inject",
rule="README-PROMPT-INJECT",
severity=severity,
file=rel_readme_path,
line=line_number,
message=f"Possible LLM prompt injection in README: {description}. "
"When AI assistants summarize this Space, injected instructions "
"may execute in the LLM context.",
owasp=["A03:2021-Injection"],
category="llm",
confidence="possible", # static only β€” cannot confirm intent
)
```
### Registration
Add to `ALL_LLM`. Tool name: `"readme-inject"`. Add to `_llm_tools` in services.
---
## TASK 08 β€” Gradio Version Vulnerability Scanner
**File:** `scanners/gradio_version_runner.py`
**Category:** `ml-security` | **Group flag:** `run_security=True`
**Binary required:** No β€” parses requirements.txt
**Tool name:** `gradio-version`
### What it does
Parses `requirements.txt`, `setup.cfg`, `pyproject.toml` to extract the
installed/pinned Gradio version. Maps version ranges to known CVEs.
Emits one finding per affected CVE with exact version evidence.
This complements pip-audit (which checks installed packages at scan time)
by working on the static requirements file β€” useful for repos not yet installed.
### Known CVE version ranges (hardcode this)
```python
GRADIO_CVES = [
{
"cve": "CVE-2023-51449",
"title": "Path traversal β€” arbitrary file read",
"affected": "<4.11.0",
"severity": "ERROR",
"owasp": "A01:2021-Broken_Access_Control",
"note": "Reads /proc/self/environ β†’ leaks all Space secrets",
},
{
"cve": "CVE-2024-1561",
"title": "Absolute path traversal via /file= endpoint",
"affected": "<4.13.0",
"severity": "ERROR",
"owasp": "A01:2021-Broken_Access_Control",
"note": "Arbitrary file read on the server",
},
{
"cve": "CVE-2026-27167",
"title": "Mocked OAuth leaks server HF token via /login/huggingface",
"affected": ">=4.16.0,<6.6.0",
"severity": "ERROR",
"owasp": "A02:2021-Cryptographic_Failures",
"note": "Any visitor to /login/huggingface steals server HF token",
},
{
"cve": "CVE-2026-28416",
"title": "SSRF via gr.load() malicious proxy_url",
"affected": "<6.6.0",
"severity": "ERROR",
"owasp": "A10:2021-Server_Side_Request_Forgery",
"note": "Attacker-controlled proxy_url reaches internal services",
},
{
"cve": "PYSEC-2024-274",
"title": "Code injection via component_meta.py Jinja2 exec()",
"affected": "<=4.36.1",
"severity": "ERROR",
"owasp": "A03:2021-Injection",
"note": "User-controlled label/prop passed to Jinja2 exec()",
},
]
```
### Use `packaging.version.Version` for comparison. Return a finding per CVE.
---
## TASK 09 β€” Live HTTP Prober Service
**File:** `sentinel/services/prober.py`
**Type:** New Sentinel service β€” separate from static scanners
**Trigger:** Called after scan completes if `probe_live=True`
### What it does
Probes a live HF Space URL for runtime vulnerabilities that cannot be
detected from static analysis alone.
### Function signatures
```python
async def probe_space(space_url: str, hf_token: str | None = None) -> list[dict]:
"""Run all live probes against a Space URL. Returns findings list."""
async def probe_file_endpoint(base_url: str) -> list[dict]:
"""CVE-2024-1561 / no-auth /file= endpoint check."""
async def probe_queue_leak(base_url: str, duration: int = 15) -> list[dict]:
"""/queue/status input data exposure check."""
async def probe_oauth_token_leak(base_url: str) -> list[dict]:
"""CVE-2026-27167 mocked OAuth check."""
async def probe_mcp_unauth(base_url: str) -> list[dict]:
"""MCP endpoint reachable without auth."""
```
### probe_file_endpoint logic
```python
# Step 1: upload a canary file via /upload if available
# Step 2: from a fresh session (no cookies), attempt to read it via /file=
# Step 3: if readable β†’ finding
# Also try known paths: /file=/etc/passwd, /file=/proc/self/environ
# Return timing data as evidence even if content is blocked
```
### probe_oauth_token_leak logic
```python
# GET {base_url}/login/huggingface
# If response sets a session cookie AND status is 200/302:
# - Decode the cookie (signed with hardcoded secret from "-v4")
# - If HF token pattern found β†’ CRITICAL finding
# This is CVE-2026-27167 β€” fixed in 6.6.0
```
### probe_queue_leak logic
```python
# Poll GET {base_url}/queue/status for `duration` seconds
# For each job in response:
# - Check if `input` or `data` field is present
# - If any field contains /tmp/gradio path β†’ try /file= fetch
# - If any field contains PII patterns (email, token) β†’ finding
```
### probe_mcp_unauth logic
```python
# GET {base_url}/mcp
# If 200 and response contains tool definitions β†’ finding
# List exposed tool names in the finding message
```
### Sentinel UI integration
Add a **"Probe Live"** button to `sentinel/templates/scan.html` that appears
only when the target has a `.hf.space` URL. POST to new route
`/api/probe/{target_id}` which calls `probe_space()` and persists results.
Do NOT integrate live probing into the main scan queue β€” keep it separate
so users consciously trigger it after confirming H1 scope.
---
## TASK 10 β€” Transformers ReDoS Semgrep Rules
**File:** `rules/ml_redos.yaml`
**Registration:** Add to `ALL_SECURITY`
**Label:** `"Semgrep:ML-ReDoS"`
### Background
Multiple ReDoS CVEs in transformers 2025/2026 affect user-controlled regex
inputs in AdamWeightDecay, EnglishNormalizer, and chat.py SETTING_RE.
### Rules to implement
**Rule 1: user-controlled-regex**
```yaml
- id: user-controlled-re-search
patterns:
- pattern: re.search($PATTERN, $INPUT, ...)
- pattern-not: re.search("...", ...)
message: >
re.search() with a user-controlled pattern is vulnerable to ReDoS.
Transformers has multiple CVEs (2025/2026) from this exact pattern.
Validate regex patterns against a complexity limit before use,
or compile with a timeout via the `regex` library (regex.search with timeout=).
severity: WARNING
languages: [python]
metadata:
owasp: ["A05:2021-Security_Misconfiguration"]
confidence: possible
category: security
```
**Rule 2: include_in_weight_decay pattern (specific transformers vector)**
```yaml
- id: transformers-redos-weight-decay
patterns:
- pattern: |
re.search($P, $N)
where:
- pattern-inside: |
def _do_use_weight_decay($NAME, ...):
...
message: >
Pattern matches the AdamWeightDecay ReDoS vector from transformers.
The include_in_weight_decay list accepts user regex that causes catastrophic
backtracking. Fixed in transformers 4.53.0.
severity: ERROR
languages: [python]
metadata:
owasp: ["A05:2021-Security_Misconfiguration"]
confidence: confirmed
category: security
```
---
## TASK 11 β€” Novel: Pickle via `__reduce__` Detector (Static)
**File:** `rules/ml_pickle_gadget.yaml`
**Registration:** Add to `ALL_SECURITY`
**Label:** `"Semgrep:ML-PickleGadget"`
### What it does (not yet documented anywhere)
Detects custom `__reduce__` implementations in code that also does
`torch.save()` or `pickle.dump()`. A class with `__reduce__` that is
serialized is a **pickle gadget** β€” if it reaches a model file,
downstream loaders (including HF users) will execute it on load.
### Rules
```yaml
- id: pickle-reduce-gadget
patterns:
- pattern: |
class $C:
...
def __reduce__(self):
return ($FN, (...))
...
message: >
Class '$C' defines __reduce__() β€” a pickle gadget. If instances of this
class are serialized via torch.save() or pickle.dump() and distributed,
anyone who loads the file will execute $FN. Verify this is intentional
and the callable is safe.
severity: WARNING
languages: [python]
metadata:
owasp: ["A08:2021-Software_and_Data_Integrity_Failures"]
confidence: possible
category: ml-security
```
```yaml
- id: pickle-reduce-with-os-system
patterns:
- pattern: |
class $C:
...
def __reduce__(self):
return (os.system, (...))
message: >
Class '$C' defines __reduce__() returning os.system β€” this is a classic
pickle RCE payload. If serialized to a model file this executes a shell
command on every user who loads the model.
severity: ERROR
languages: [python]
metadata:
owasp: ["A08:2021-Software_and_Data_Integrity_Failures"]
confidence: confirmed
category: ml-security
```
---
## TASK 12 β€” Novel: Chat Template Injection Detector (Static)
**File:** `rules/ml_chat_template.yaml`
**Registration:** Add to `ALL_LLM`
**Label:** `"Semgrep:ML-ChatTemplate"`
### What it does (not documented as a scanner rule anywhere)
Detects user-controlled strings being interpolated into chat templates
before `apply_chat_template()`. If the template uses Jinja2 and the
content is not escaped, an attacker can inject role boundaries, system
messages, or special tokens.
### Rules
```yaml
- id: chat-template-user-input-fstring
patterns:
- pattern: |
$MSGS = [{"role": "user", "content": f"...{$X}..."}]
...
$T.apply_chat_template($MSGS, ...)
- pattern-not-inside: |
$X = "..."
message: >
User input $X is interpolated into a chat message before apply_chat_template().
If $X contains role boundary tokens (<|im_end|>, [/INST], etc.) the chat
template may parse them as structural markers, escaping the user role.
Sanitize or escape special tokens before interpolation.
severity: WARNING
languages: [python]
metadata:
owasp: ["A03:2021-Injection"]
confidence: possible
category: llm
```
```yaml
- id: system-prompt-user-concat
patterns:
- pattern: |
{"role": "system", "content": $X + $Y}
- pattern-not: |
{"role": "system", "content": "..." + "..."}
message: >
System prompt content built by string concatenation with a variable.
If either operand is user-controlled, the attacker controls the system prompt.
severity: ERROR
languages: [python]
metadata:
owasp: ["A03:2021-Injection"]
confidence: likely
category: llm
```
---
## TASK 13 β€” Remediation Entries
**File:** `report/remediation.py`
Add entries for all new rule IDs. Pattern: `"RULE-ID": "plain English fix"`.
```python
# CVE-specific
"CVE-2026-27167": "Upgrade gradio to >=6.6.0. The mocked OAuth route at /login/huggingface leaks the server's HF token in versions 4.16.0–6.5.x.",
"CVE-2026-28416": "Upgrade gradio to >=6.6.0. Validate Space names against an allowlist before passing to gr.load().",
"CVE-2026-1839": "Use torch.load(..., weights_only=True) and upgrade to torch>=2.6 and transformers>=5.0.0rc3.",
"CVE-2025-32434": "Upgrade torch to >=2.6. weights_only=True alone is insufficient on earlier versions.",
"CVE-2025-1550": "Use keras.models.load_model(..., safe_mode=True) (Keras 3+) or validate file source.",
# Rule-specific
"GRADIO-GLOBAL-STATE": "Move gr.State() inside the function that uses it, or use session-scoped storage.",
"trust-remote-code-user-input": "Hardcode the model name or validate against an allowlist. Never pass user input to trust_remote_code=True.",
"torch-load-missing-weights-only": "Add weights_only=True and upgrade to torch>=2.6. Prefer safetensors format.",
"load-dataset-user-input": "Hardcode the dataset name. Never pass user input to load_dataset().",
"joblib-load-user-input": "Validate the file path against an allowlist. Prefer safer serialization formats.",
"numpy-allow-pickle-user-input": "Remove allow_pickle=True. Use allow_pickle=False (safe default since numpy 1.16.3).",
"safetensors-metadata-eval": "Never eval() or import from safetensors metadata. Treat metadata as untrusted input.",
"keras-load-model-user-input": "Use safe_mode=True or validate file origin before loading.",
"gr-load-ssrf": "Hardcode the Space name. Never pass user input to gr.load().",
"gradio-mocked-oauth-token-leak": "Upgrade gradio to >=6.6.0 or remove gr.LoginButton when running outside HF Spaces.",
"from-pretrained-no-revision-pin": "Add revision='<commit-sha>' to pin to a specific trusted commit.",
"gradio-unbounded-numeric-input": "Add maximum=<N> to gr.Number() and enforce server-side with min(user_val, MAX).",
"mcp-server-no-auth": "Add auth=('user','pass') to demo.launch() or restrict MCP-exposed functions.",
"mcp-dynamic-docstring": "Use a static string literal for function __doc__. Never build from user input.",
"README-PROMPT-INJECT": "Remove instruction-like patterns from README. Use plain descriptive text only.",
"pickle-reduce-gadget": "Audit __reduce__() usage. Ensure serialized objects cannot execute system calls.",
"pickle-reduce-with-os-system": "Remove os.system from __reduce__(). This is a live pickle RCE payload.",
"chat-template-user-input-fstring": "Escape special tokens before interpolation, or use the tokenizer's built-in sanitization.",
"system-prompt-user-concat": "Never concatenate user input into the system role. Use a hardcoded system prompt only.",
"user-controlled-re-search": "Validate regex complexity before use or use the `regex` library with timeout parameter.",
```
---
## TASK 14 β€” Registration Checklist
**After completing all tasks above, verify every item:**
```
[ ] scanners/__init__.py β€” exports: cve_trigger, gradio_state, gradio_version, readme_inject
[ ] core/scanner.py β€” _TASK_TO_TOOL has all 4 new tool names
[ ] core/scanner.py β€” scan_repo() task list includes all 4
[ ] rules/__init__.py β€” ALL_SECURITY includes Tasks 03,04,05,08,10,11
[ ] rules/__init__.py β€” ALL_LLM includes Tasks 06,12
[ ] sentinel/routes/discover.py β€” _ALLOWED_SCANNERS has all new names
[ ] sentinel/services/scanner.py β€” _TOOL_NAMES, _sec_tools, _llm_tools updated
[ ] sentinel/services/prober.py β€” created (Task 09)
[ ] sentinel/routes/ β€” new /api/probe/{id} route added
[ ] sentinel/templates/scan.html β€” "Probe Live" button added
[ ] report/remediation.py β€” all new rule IDs added (Task 13)
[ ] tests/test_cve_trigger.py β€” created
[ ] tests/test_gradio_state.py β€” created
[ ] tests/test_gradio_version.py β€” created
[ ] tests/test_readme_inject.py β€” created
```
---
## Testing conventions
Follow `tests/test_radon_runner.py` as template. Every runner needs:
1. `test_not_installed_returns_empty` (or `test_no_requirements_file`)
2. `test_no_findings_on_clean_input`
3. `test_detects_known_bad_pattern`
4. `test_ignores_safe_variant` (e.g. weights_only=True)
5. `test_severity_escalation` where applicable
6. `test_correct_owasp_category`
Run with:
```bash
pytest tests/ -m "not slow and not hf" -q
```