mcp-authscan 0.8.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.
- mcp_authscan-0.8.0/LICENSE +21 -0
- mcp_authscan-0.8.0/PKG-INFO +238 -0
- mcp_authscan-0.8.0/README.md +215 -0
- mcp_authscan-0.8.0/mcp_authscan.egg-info/PKG-INFO +238 -0
- mcp_authscan-0.8.0/mcp_authscan.egg-info/SOURCES.txt +9 -0
- mcp_authscan-0.8.0/mcp_authscan.egg-info/dependency_links.txt +1 -0
- mcp_authscan-0.8.0/mcp_authscan.egg-info/entry_points.txt +2 -0
- mcp_authscan-0.8.0/mcp_authscan.egg-info/top_level.txt +1 -0
- mcp_authscan-0.8.0/mcp_authscan.py +791 -0
- mcp_authscan-0.8.0/pyproject.toml +37 -0
- mcp_authscan-0.8.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leroy H. Mason
|
|
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.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-authscan
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Static security scanner for self-rolled OAuth/auth failure classes in MCP servers, gateways, and OAuth clients. Rules seeded from live-reproduced, chain-of-custody-anchored VATA findings. SARIF output for GitHub code scanning.
|
|
5
|
+
Author: Leroy H. Mason
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/LHMisme420/mcp-authscan
|
|
8
|
+
Project-URL: Repository, https://github.com/LHMisme420/mcp-authscan
|
|
9
|
+
Project-URL: Dashboard, https://lhmisme420.github.io/VATA-SCORES-0311
|
|
10
|
+
Keywords: mcp,security,oauth,pkce,sast,sarif,scanner,vulnerability
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# mcp-authscan
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+

|
|
28
|
+

|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
> Static security scanner for the classic web-auth failures that keep shipping in
|
|
32
|
+
> MCP servers, gateways, and OAuth clients — the boring bugs the AI-native scanners skip.
|
|
33
|
+
|
|
34
|
+
*Built and maintained by **Leroy H. Mason** (RU∞X / VATA) — an independent security
|
|
35
|
+
researcher applying two decades of law-enforcement and risk-management
|
|
36
|
+
chain-of-custody discipline to AI-agent security.*
|
|
37
|
+
|
|
38
|
+
Most MCP security tools chase AI-native failures (tool poisoning, prompt injection,
|
|
39
|
+
rug pulls). `mcp-authscan` targets the plain web-auth failures that keep shipping in
|
|
40
|
+
MCP gateway and OAuth code — authorization servers that skip PKCE, authorization
|
|
41
|
+
codes that replay or never expire, issuer-binding gaps in M2M clients, hardcoded
|
|
42
|
+
admin credentials. **Every rule is seeded from a vulnerability VATA found and
|
|
43
|
+
live-reproduced in a real MCP project.** Stdlib Python 3, zero dependencies.
|
|
44
|
+
|
|
45
|
+
**What makes it different:** every scan emits a SHA-256 over its findings that you can
|
|
46
|
+
anchor to a public blockchain — a tamper-evident, timestamped receipt. Commodity
|
|
47
|
+
scanners give you output; this gives you proof. *Receipts Over Promises.*
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
git clone https://github.com/LHMisme420/mcp-authscan.git
|
|
53
|
+
python3 mcp-authscan/mcp_authscan.py <path-to-repo> # scan
|
|
54
|
+
python3 mcp-authscan/mcp_authscan.py <path> --sarif # SARIF for GitHub code scanning
|
|
55
|
+
python3 mcp-authscan/mcp_authscan.py <path> --json # emits report_sha256 to anchor
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
No install step, no dependencies — stdlib Python 3. *(PyPI package coming next.)*
|
|
59
|
+
|
|
60
|
+
## Findings behind the rules
|
|
61
|
+
|
|
62
|
+
`mcp-authscan` is not theoretical. Its rules encode real vulnerabilities VATA
|
|
63
|
+
discovered, live-reproduced, and disclosed through coordinated channels:
|
|
64
|
+
|
|
65
|
+
- **PraisonAI** — CVE-2026-60085 (GHSA-5r6c-gj4g-r697), unenforced security policy in
|
|
66
|
+
the subprocess sandbox; fixed in 4.6.78, credited to LHMisme420.
|
|
67
|
+
- **B-series rules (PKCE / authorization-code lifecycle)** are seeded from findings
|
|
68
|
+
live-reproduced in the webrix, atrawog, and akshay5995 self-rolled OAuth gateways —
|
|
69
|
+
each anchored in the VATA corpus below.
|
|
70
|
+
|
|
71
|
+
Every finding is reproduced before disclosure and anchored on-chain for
|
|
72
|
+
chain-of-custody. Full corpus → [VATA dashboard](https://lhmisme420.github.io/VATA-SCORES-0311)
|
|
73
|
+
· [Zenodo (DOI, ORCID-linked)](https://doi.org/10.5281/zenodo.21839413)
|
|
74
|
+
|
|
75
|
+
## Rules
|
|
76
|
+
|
|
77
|
+
| ID | Sev / Conf | Class | Type |
|
|
78
|
+
|----|-----------|-------|------|
|
|
79
|
+
| A1 | CRITICAL / HIGH | OAuth authorize handler reads `client_id`/`redirect_uri` but never validates against an allowlist (auth-bypass / open-redirect / ATO) | detector |
|
|
80
|
+
| A2 | CRITICAL–HIGH | Hardcoded default admin credentials; admin re-seed routines run on restart | detector |
|
|
81
|
+
| A3 | HIGH / MEDIUM | Auth middleware validates a token but enforces no role/tenant/scope (cross-tenant, CWE-863) | detector |
|
|
82
|
+
| A4 | MEDIUM / LOW | SSRF surface: outbound HTTP request to a non-literal, externally-influenced URL with no allowlist / internal-IP guard | review-list |
|
|
83
|
+
| A5 | LOW | `excluded_tools`/blocklist defined but enforcement unverified across surfaces | review pointer |
|
|
84
|
+
| C1 | HIGH / HIGH (MEDIUM when options are spread) | M2M MCP client provider built without an issuer binding: Python `ClientCredentialsOAuthProvider`/`PrivateKeyJWTOAuthProvider` without `issuer=`, TS `ClientCredentialsProvider`/`PrivateKeyJwtProvider`/`StaticPrivateKeyJwtProvider`/`CrossAppAccessProvider` without `expectedIssuer` | detector |
|
|
85
|
+
| C2 | HIGH / HIGH | Deprecated Python `RFC7523OAuthClientProvider` — has no issuer option, cannot be bound by config | detector |
|
|
86
|
+
| C3 | HIGH / MEDIUM | Issuer validation switched off (`skipIssuerMetadataValidation: true`, `verify_iss: False`, etc.) | detector |
|
|
87
|
+
| B1 | HIGH / HIGH | Self-rolled authorization server told to skip PKCE (`skipLocalPkceValidation: true`) with nothing else verifying the challenge (CWE-287) | detector |
|
|
88
|
+
| B1 | MEDIUM / MEDIUM | AS receives a PKCE `code_verifier` as input but no SHA-256/S256 transform appears in the file — verifier never checked against the stored challenge (CWE-287) | review-list |
|
|
89
|
+
| B2 | HIGH / MEDIUM | Authorization code redeemed at the token endpoint but never invalidated (delete/mark-used/revoke) in the same file — possible replay (CWE-294) | review-list |
|
|
90
|
+
| B3 | HIGH / HIGH | Authorization code issued with a lifetime far exceeding RFC 6749's ~600s recommendation (CWE-613) | detector |
|
|
91
|
+
|
|
92
|
+
A1–A3 are detectors: a hit is a finding to triage. A4–A5 are review-lists:
|
|
93
|
+
SSRF and enforcement-gap detection need dataflow, not pattern matching, so these
|
|
94
|
+
enumerate the surface to inspect rather than claiming a bug. Confidence labels
|
|
95
|
+
say which is which — read them.
|
|
96
|
+
|
|
97
|
+
### Why the C-series exists
|
|
98
|
+
|
|
99
|
+
Upgrading the MCP SDK does not close the client-side issuer gap. The fixed
|
|
100
|
+
versions only enforce issuer binding on M2M providers when the issuer is
|
|
101
|
+
configured; without it, pre-provisioned client secrets and signed JWT
|
|
102
|
+
assertions are sent to whatever authorization server the MCP server
|
|
103
|
+
advertises. Version-based scanners report a pass on code that is still
|
|
104
|
+
exposed. Refs: python-sdk GHSA-qx49-fqc8-xw99; typescript-sdk PR #2887
|
|
105
|
+
(SEP-2352). C1 is a call-site check: options built in another file show up
|
|
106
|
+
as MEDIUM-confidence review items.
|
|
107
|
+
|
|
108
|
+
A runtime proof of the C1/C2 gap - a rogue authorization server harvesting
|
|
109
|
+
the client's credentials across all three M2M shapes, and refusing once
|
|
110
|
+
issuer= is set - is in
|
|
111
|
+
rule_corpus/client_issuer_binding/live_repro/ (--json emits a
|
|
112
|
+
report_sha256 to anchor).
|
|
113
|
+
|
|
114
|
+
### Why the B-series exists
|
|
115
|
+
|
|
116
|
+
Every self-rolled OAuth 2.1 authorization server VATA has audited that
|
|
117
|
+
hand-rolls RFC 6749 / PKCE has failed at least one of three lifecycle
|
|
118
|
+
invariants; gateways delegating to an established IdP (Keycloak, Cognito, Dex)
|
|
119
|
+
have not. The B-series encodes the three most-repeated failures, seeded from
|
|
120
|
+
live-repro'd findings:
|
|
121
|
+
|
|
122
|
+
- **B1 — PKCE accepted but never enforced.** Either the provider is explicitly
|
|
123
|
+
told to skip local PKCE (`skipLocalPkceValidation: true`, webrix) or a
|
|
124
|
+
`code_verifier` is accepted but never hashed and compared (atrawog, akshay5995).
|
|
125
|
+
- **B2 — authorization code not single-use.** The code is redeemed but never
|
|
126
|
+
invalidated, so it replays (webrix).
|
|
127
|
+
- **B3 — authorization code never expires / excessive TTL.** Codes issued with
|
|
128
|
+
lifetimes orders of magnitude over the ~10-minute recommendation (atrawog, 1yr).
|
|
129
|
+
|
|
130
|
+
Honest limits, stated so findings are not over-claimed:
|
|
131
|
+
|
|
132
|
+
- B1 does **not** detect *optional* PKCE (validated when present, skippable when
|
|
133
|
+
absent) — that is a control-flow property, not a pattern, and is left to manual
|
|
134
|
+
review rather than guessed at.
|
|
135
|
+
- B2 is **same-file**: a server that invalidates its code in a separate storage
|
|
136
|
+
module will be flagged here as a review item (MEDIUM), not asserted as a bug.
|
|
137
|
+
Confirm the storage layer before filing.
|
|
138
|
+
- B3 fires only on an **explicit** excessive number tied to an authorization
|
|
139
|
+
code; expiry that is simply *absent* across a schema (a cross-file absence) is
|
|
140
|
+
not detected.
|
|
141
|
+
|
|
142
|
+
B1-skip and B3 are detectors; B1-verifier and B2 are review-lists, for the same
|
|
143
|
+
reason A4/A5 are — the certain cases are findings, the dataflow-dependent cases
|
|
144
|
+
enumerate surface to inspect. The confidence column says which.
|
|
145
|
+
|
|
146
|
+
## Usage (all options)
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
python3 mcp_authscan.py <path> --include-tests # test files skipped by default
|
|
150
|
+
python3 mcp_authscan.py <path> --fail-on high # CI gate: nonzero exit on >= sev
|
|
151
|
+
python3 mcp_authscan.py <path> --exclude DIR # skip a dir (repeatable)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Ground truth
|
|
155
|
+
|
|
156
|
+
The rules are validated by re-detecting findings already filed and anchored by
|
|
157
|
+
VATA. A rule that stops re-detecting its seed finding is a regression, not a
|
|
158
|
+
release.
|
|
159
|
+
|
|
160
|
+
| Repo | Rule | Location |
|
|
161
|
+
|------|------|----------|
|
|
162
|
+
| lucky-aeon/mcp-gateway | A1 | `internal/gateway/auth.go` `handleOAuthAuthorize` |
|
|
163
|
+
| lucky-aeon/mcp-gateway | A2 | `internal/platform/config/config.go` defaults |
|
|
164
|
+
| lucky-aeon/mcp-gateway | A3 | `internal/gateway/auth.go` `mcpAuthMiddleware` |
|
|
165
|
+
| mcpjungle/mcpjungle | A4 | `internal/service/mcp/upstream_oauth.go` `RegistrationEndpoint` fetch |
|
|
166
|
+
| rule_corpus fixture | C1–C3 | `rule_corpus/client_issuer_binding` (bound providers must stay silent) |
|
|
167
|
+
| webrix-ai/secure-mcp-gateway | B1, B2 | `src/services/mcp-auth-provider.ts` (`skipLocalPkceValidation`, `exchangeAuthorizationCode`) |
|
|
168
|
+
| atrawog/mcp-oauth-dynamicclient | B1, B3 | `src/mcp_oauth_dynamicclient/routes.py` (1-year auth-code TTL) |
|
|
169
|
+
| akshay5995/mcp-oauth-gateway | B1 | `src/gateway.py` (`code_verifier` received, never validated) |
|
|
170
|
+
|
|
171
|
+
## Provenance
|
|
172
|
+
|
|
173
|
+
Every scan is anchorable. `--json` emits a `report_sha256` over the findings;
|
|
174
|
+
anchor it to Ethereum before disclosure so the report is timestamped and
|
|
175
|
+
tamper-evident. That chain-of-custody is the differentiator — commodity scanners
|
|
176
|
+
don't give you a receipt.
|
|
177
|
+
|
|
178
|
+
Dashboard: https://lhmisme420.github.io/VATA-SCORES-0311
|
|
179
|
+
|
|
180
|
+
## Limitations
|
|
181
|
+
|
|
182
|
+
Heuristic static analysis. It flags patterns; it does not prove exploitability.
|
|
183
|
+
A4/A5 are review-lists by design. **Absence of a finding is not proof of safety.**
|
|
184
|
+
|
|
185
|
+
## GitHub code scanning (SARIF)
|
|
186
|
+
|
|
187
|
+
`mcp-authscan` can emit SARIF 2.1.0 so findings surface natively in a repo's
|
|
188
|
+
**Security → Code scanning** tab. Each rule carries a `security-severity` score
|
|
189
|
+
and its CWE identifier, so GitHub renders severity and taxonomy with no extra
|
|
190
|
+
configuration.
|
|
191
|
+
|
|
192
|
+
Result level follows severity: CRITICAL/HIGH map to `error`, MEDIUM to
|
|
193
|
+
`warning`, LOW to `note` — so review-list (MEDIUM) findings surface as
|
|
194
|
+
warnings, not errors, in the Security tab.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# write SARIF to a file
|
|
198
|
+
mcp-authscan . --sarif results.sarif
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
To scan on every push/PR and upload results, drop this into a consuming repo at
|
|
202
|
+
`.github/workflows/mcp-authscan.yml` (identical copy shipped at
|
|
203
|
+
`examples/github-code-scanning.yml`):
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
name: MCP auth scan
|
|
207
|
+
on:
|
|
208
|
+
push:
|
|
209
|
+
branches: [main]
|
|
210
|
+
pull_request:
|
|
211
|
+
schedule:
|
|
212
|
+
- cron: '0 6 * * 1'
|
|
213
|
+
|
|
214
|
+
permissions:
|
|
215
|
+
contents: read
|
|
216
|
+
security-events: write
|
|
217
|
+
|
|
218
|
+
jobs:
|
|
219
|
+
mcp-authscan:
|
|
220
|
+
runs-on: ubuntu-latest
|
|
221
|
+
steps:
|
|
222
|
+
- uses: actions/checkout@v4
|
|
223
|
+
- uses: actions/setup-python@v5
|
|
224
|
+
with:
|
|
225
|
+
python-version: '3.12'
|
|
226
|
+
- run: pip install git+https://github.com/LHMisme420/mcp-authscan.git
|
|
227
|
+
- run: mcp-authscan . --sarif results.sarif
|
|
228
|
+
continue-on-error: true
|
|
229
|
+
- if: always()
|
|
230
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
231
|
+
with:
|
|
232
|
+
sarif_file: results.sarif
|
|
233
|
+
category: mcp-authscan
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`security-events: write` is mandatory for the upload. `continue-on-error` +
|
|
237
|
+
`if: always()` ensure findings still reach the Security tab when the scan exits
|
|
238
|
+
non-zero on a hit.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# mcp-authscan
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
> Static security scanner for the classic web-auth failures that keep shipping in
|
|
9
|
+
> MCP servers, gateways, and OAuth clients — the boring bugs the AI-native scanners skip.
|
|
10
|
+
|
|
11
|
+
*Built and maintained by **Leroy H. Mason** (RU∞X / VATA) — an independent security
|
|
12
|
+
researcher applying two decades of law-enforcement and risk-management
|
|
13
|
+
chain-of-custody discipline to AI-agent security.*
|
|
14
|
+
|
|
15
|
+
Most MCP security tools chase AI-native failures (tool poisoning, prompt injection,
|
|
16
|
+
rug pulls). `mcp-authscan` targets the plain web-auth failures that keep shipping in
|
|
17
|
+
MCP gateway and OAuth code — authorization servers that skip PKCE, authorization
|
|
18
|
+
codes that replay or never expire, issuer-binding gaps in M2M clients, hardcoded
|
|
19
|
+
admin credentials. **Every rule is seeded from a vulnerability VATA found and
|
|
20
|
+
live-reproduced in a real MCP project.** Stdlib Python 3, zero dependencies.
|
|
21
|
+
|
|
22
|
+
**What makes it different:** every scan emits a SHA-256 over its findings that you can
|
|
23
|
+
anchor to a public blockchain — a tamper-evident, timestamped receipt. Commodity
|
|
24
|
+
scanners give you output; this gives you proof. *Receipts Over Promises.*
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
git clone https://github.com/LHMisme420/mcp-authscan.git
|
|
30
|
+
python3 mcp-authscan/mcp_authscan.py <path-to-repo> # scan
|
|
31
|
+
python3 mcp-authscan/mcp_authscan.py <path> --sarif # SARIF for GitHub code scanning
|
|
32
|
+
python3 mcp-authscan/mcp_authscan.py <path> --json # emits report_sha256 to anchor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
No install step, no dependencies — stdlib Python 3. *(PyPI package coming next.)*
|
|
36
|
+
|
|
37
|
+
## Findings behind the rules
|
|
38
|
+
|
|
39
|
+
`mcp-authscan` is not theoretical. Its rules encode real vulnerabilities VATA
|
|
40
|
+
discovered, live-reproduced, and disclosed through coordinated channels:
|
|
41
|
+
|
|
42
|
+
- **PraisonAI** — CVE-2026-60085 (GHSA-5r6c-gj4g-r697), unenforced security policy in
|
|
43
|
+
the subprocess sandbox; fixed in 4.6.78, credited to LHMisme420.
|
|
44
|
+
- **B-series rules (PKCE / authorization-code lifecycle)** are seeded from findings
|
|
45
|
+
live-reproduced in the webrix, atrawog, and akshay5995 self-rolled OAuth gateways —
|
|
46
|
+
each anchored in the VATA corpus below.
|
|
47
|
+
|
|
48
|
+
Every finding is reproduced before disclosure and anchored on-chain for
|
|
49
|
+
chain-of-custody. Full corpus → [VATA dashboard](https://lhmisme420.github.io/VATA-SCORES-0311)
|
|
50
|
+
· [Zenodo (DOI, ORCID-linked)](https://doi.org/10.5281/zenodo.21839413)
|
|
51
|
+
|
|
52
|
+
## Rules
|
|
53
|
+
|
|
54
|
+
| ID | Sev / Conf | Class | Type |
|
|
55
|
+
|----|-----------|-------|------|
|
|
56
|
+
| A1 | CRITICAL / HIGH | OAuth authorize handler reads `client_id`/`redirect_uri` but never validates against an allowlist (auth-bypass / open-redirect / ATO) | detector |
|
|
57
|
+
| A2 | CRITICAL–HIGH | Hardcoded default admin credentials; admin re-seed routines run on restart | detector |
|
|
58
|
+
| A3 | HIGH / MEDIUM | Auth middleware validates a token but enforces no role/tenant/scope (cross-tenant, CWE-863) | detector |
|
|
59
|
+
| A4 | MEDIUM / LOW | SSRF surface: outbound HTTP request to a non-literal, externally-influenced URL with no allowlist / internal-IP guard | review-list |
|
|
60
|
+
| A5 | LOW | `excluded_tools`/blocklist defined but enforcement unverified across surfaces | review pointer |
|
|
61
|
+
| C1 | HIGH / HIGH (MEDIUM when options are spread) | M2M MCP client provider built without an issuer binding: Python `ClientCredentialsOAuthProvider`/`PrivateKeyJWTOAuthProvider` without `issuer=`, TS `ClientCredentialsProvider`/`PrivateKeyJwtProvider`/`StaticPrivateKeyJwtProvider`/`CrossAppAccessProvider` without `expectedIssuer` | detector |
|
|
62
|
+
| C2 | HIGH / HIGH | Deprecated Python `RFC7523OAuthClientProvider` — has no issuer option, cannot be bound by config | detector |
|
|
63
|
+
| C3 | HIGH / MEDIUM | Issuer validation switched off (`skipIssuerMetadataValidation: true`, `verify_iss: False`, etc.) | detector |
|
|
64
|
+
| B1 | HIGH / HIGH | Self-rolled authorization server told to skip PKCE (`skipLocalPkceValidation: true`) with nothing else verifying the challenge (CWE-287) | detector |
|
|
65
|
+
| B1 | MEDIUM / MEDIUM | AS receives a PKCE `code_verifier` as input but no SHA-256/S256 transform appears in the file — verifier never checked against the stored challenge (CWE-287) | review-list |
|
|
66
|
+
| B2 | HIGH / MEDIUM | Authorization code redeemed at the token endpoint but never invalidated (delete/mark-used/revoke) in the same file — possible replay (CWE-294) | review-list |
|
|
67
|
+
| B3 | HIGH / HIGH | Authorization code issued with a lifetime far exceeding RFC 6749's ~600s recommendation (CWE-613) | detector |
|
|
68
|
+
|
|
69
|
+
A1–A3 are detectors: a hit is a finding to triage. A4–A5 are review-lists:
|
|
70
|
+
SSRF and enforcement-gap detection need dataflow, not pattern matching, so these
|
|
71
|
+
enumerate the surface to inspect rather than claiming a bug. Confidence labels
|
|
72
|
+
say which is which — read them.
|
|
73
|
+
|
|
74
|
+
### Why the C-series exists
|
|
75
|
+
|
|
76
|
+
Upgrading the MCP SDK does not close the client-side issuer gap. The fixed
|
|
77
|
+
versions only enforce issuer binding on M2M providers when the issuer is
|
|
78
|
+
configured; without it, pre-provisioned client secrets and signed JWT
|
|
79
|
+
assertions are sent to whatever authorization server the MCP server
|
|
80
|
+
advertises. Version-based scanners report a pass on code that is still
|
|
81
|
+
exposed. Refs: python-sdk GHSA-qx49-fqc8-xw99; typescript-sdk PR #2887
|
|
82
|
+
(SEP-2352). C1 is a call-site check: options built in another file show up
|
|
83
|
+
as MEDIUM-confidence review items.
|
|
84
|
+
|
|
85
|
+
A runtime proof of the C1/C2 gap - a rogue authorization server harvesting
|
|
86
|
+
the client's credentials across all three M2M shapes, and refusing once
|
|
87
|
+
issuer= is set - is in
|
|
88
|
+
rule_corpus/client_issuer_binding/live_repro/ (--json emits a
|
|
89
|
+
report_sha256 to anchor).
|
|
90
|
+
|
|
91
|
+
### Why the B-series exists
|
|
92
|
+
|
|
93
|
+
Every self-rolled OAuth 2.1 authorization server VATA has audited that
|
|
94
|
+
hand-rolls RFC 6749 / PKCE has failed at least one of three lifecycle
|
|
95
|
+
invariants; gateways delegating to an established IdP (Keycloak, Cognito, Dex)
|
|
96
|
+
have not. The B-series encodes the three most-repeated failures, seeded from
|
|
97
|
+
live-repro'd findings:
|
|
98
|
+
|
|
99
|
+
- **B1 — PKCE accepted but never enforced.** Either the provider is explicitly
|
|
100
|
+
told to skip local PKCE (`skipLocalPkceValidation: true`, webrix) or a
|
|
101
|
+
`code_verifier` is accepted but never hashed and compared (atrawog, akshay5995).
|
|
102
|
+
- **B2 — authorization code not single-use.** The code is redeemed but never
|
|
103
|
+
invalidated, so it replays (webrix).
|
|
104
|
+
- **B3 — authorization code never expires / excessive TTL.** Codes issued with
|
|
105
|
+
lifetimes orders of magnitude over the ~10-minute recommendation (atrawog, 1yr).
|
|
106
|
+
|
|
107
|
+
Honest limits, stated so findings are not over-claimed:
|
|
108
|
+
|
|
109
|
+
- B1 does **not** detect *optional* PKCE (validated when present, skippable when
|
|
110
|
+
absent) — that is a control-flow property, not a pattern, and is left to manual
|
|
111
|
+
review rather than guessed at.
|
|
112
|
+
- B2 is **same-file**: a server that invalidates its code in a separate storage
|
|
113
|
+
module will be flagged here as a review item (MEDIUM), not asserted as a bug.
|
|
114
|
+
Confirm the storage layer before filing.
|
|
115
|
+
- B3 fires only on an **explicit** excessive number tied to an authorization
|
|
116
|
+
code; expiry that is simply *absent* across a schema (a cross-file absence) is
|
|
117
|
+
not detected.
|
|
118
|
+
|
|
119
|
+
B1-skip and B3 are detectors; B1-verifier and B2 are review-lists, for the same
|
|
120
|
+
reason A4/A5 are — the certain cases are findings, the dataflow-dependent cases
|
|
121
|
+
enumerate surface to inspect. The confidence column says which.
|
|
122
|
+
|
|
123
|
+
## Usage (all options)
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
python3 mcp_authscan.py <path> --include-tests # test files skipped by default
|
|
127
|
+
python3 mcp_authscan.py <path> --fail-on high # CI gate: nonzero exit on >= sev
|
|
128
|
+
python3 mcp_authscan.py <path> --exclude DIR # skip a dir (repeatable)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Ground truth
|
|
132
|
+
|
|
133
|
+
The rules are validated by re-detecting findings already filed and anchored by
|
|
134
|
+
VATA. A rule that stops re-detecting its seed finding is a regression, not a
|
|
135
|
+
release.
|
|
136
|
+
|
|
137
|
+
| Repo | Rule | Location |
|
|
138
|
+
|------|------|----------|
|
|
139
|
+
| lucky-aeon/mcp-gateway | A1 | `internal/gateway/auth.go` `handleOAuthAuthorize` |
|
|
140
|
+
| lucky-aeon/mcp-gateway | A2 | `internal/platform/config/config.go` defaults |
|
|
141
|
+
| lucky-aeon/mcp-gateway | A3 | `internal/gateway/auth.go` `mcpAuthMiddleware` |
|
|
142
|
+
| mcpjungle/mcpjungle | A4 | `internal/service/mcp/upstream_oauth.go` `RegistrationEndpoint` fetch |
|
|
143
|
+
| rule_corpus fixture | C1–C3 | `rule_corpus/client_issuer_binding` (bound providers must stay silent) |
|
|
144
|
+
| webrix-ai/secure-mcp-gateway | B1, B2 | `src/services/mcp-auth-provider.ts` (`skipLocalPkceValidation`, `exchangeAuthorizationCode`) |
|
|
145
|
+
| atrawog/mcp-oauth-dynamicclient | B1, B3 | `src/mcp_oauth_dynamicclient/routes.py` (1-year auth-code TTL) |
|
|
146
|
+
| akshay5995/mcp-oauth-gateway | B1 | `src/gateway.py` (`code_verifier` received, never validated) |
|
|
147
|
+
|
|
148
|
+
## Provenance
|
|
149
|
+
|
|
150
|
+
Every scan is anchorable. `--json` emits a `report_sha256` over the findings;
|
|
151
|
+
anchor it to Ethereum before disclosure so the report is timestamped and
|
|
152
|
+
tamper-evident. That chain-of-custody is the differentiator — commodity scanners
|
|
153
|
+
don't give you a receipt.
|
|
154
|
+
|
|
155
|
+
Dashboard: https://lhmisme420.github.io/VATA-SCORES-0311
|
|
156
|
+
|
|
157
|
+
## Limitations
|
|
158
|
+
|
|
159
|
+
Heuristic static analysis. It flags patterns; it does not prove exploitability.
|
|
160
|
+
A4/A5 are review-lists by design. **Absence of a finding is not proof of safety.**
|
|
161
|
+
|
|
162
|
+
## GitHub code scanning (SARIF)
|
|
163
|
+
|
|
164
|
+
`mcp-authscan` can emit SARIF 2.1.0 so findings surface natively in a repo's
|
|
165
|
+
**Security → Code scanning** tab. Each rule carries a `security-severity` score
|
|
166
|
+
and its CWE identifier, so GitHub renders severity and taxonomy with no extra
|
|
167
|
+
configuration.
|
|
168
|
+
|
|
169
|
+
Result level follows severity: CRITICAL/HIGH map to `error`, MEDIUM to
|
|
170
|
+
`warning`, LOW to `note` — so review-list (MEDIUM) findings surface as
|
|
171
|
+
warnings, not errors, in the Security tab.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# write SARIF to a file
|
|
175
|
+
mcp-authscan . --sarif results.sarif
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
To scan on every push/PR and upload results, drop this into a consuming repo at
|
|
179
|
+
`.github/workflows/mcp-authscan.yml` (identical copy shipped at
|
|
180
|
+
`examples/github-code-scanning.yml`):
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
name: MCP auth scan
|
|
184
|
+
on:
|
|
185
|
+
push:
|
|
186
|
+
branches: [main]
|
|
187
|
+
pull_request:
|
|
188
|
+
schedule:
|
|
189
|
+
- cron: '0 6 * * 1'
|
|
190
|
+
|
|
191
|
+
permissions:
|
|
192
|
+
contents: read
|
|
193
|
+
security-events: write
|
|
194
|
+
|
|
195
|
+
jobs:
|
|
196
|
+
mcp-authscan:
|
|
197
|
+
runs-on: ubuntu-latest
|
|
198
|
+
steps:
|
|
199
|
+
- uses: actions/checkout@v4
|
|
200
|
+
- uses: actions/setup-python@v5
|
|
201
|
+
with:
|
|
202
|
+
python-version: '3.12'
|
|
203
|
+
- run: pip install git+https://github.com/LHMisme420/mcp-authscan.git
|
|
204
|
+
- run: mcp-authscan . --sarif results.sarif
|
|
205
|
+
continue-on-error: true
|
|
206
|
+
- if: always()
|
|
207
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
208
|
+
with:
|
|
209
|
+
sarif_file: results.sarif
|
|
210
|
+
category: mcp-authscan
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`security-events: write` is mandatory for the upload. `continue-on-error` +
|
|
214
|
+
`if: always()` ensure findings still reach the Security tab when the scan exits
|
|
215
|
+
non-zero on a hit.
|