io.github.hidai25/evalview-mcp
version 0.6.0
- scanned 2026-10-16
- source github.com/hidai25/eval-view
- transport stdio
- files read 176
- methodology 0.2.0
- ruleset 0.1.1
25
100measured on 65% of this axis
not assessed
no lockfile in the fetched source; dependency versions are unresolved, so no vulnerability can be attributed
88measured on 50% of this axis
24
The evidence
Every deduction below names the artefact that produced it. Nothing on this page was executed: the source was read, never run, and no running server was contacted.
Code Safety
- evalview/chat.py:1947mcp-python-shell-true-nonliteral (critical/medium)
A subprocess is spawned with shell=True on a value that is not a string literal. Any caller-supplied component of that value is shell-injectable: `; rm -rf ~` in an argument is executed, not passed.
# Use the same spinner style for tool execution with console.status(f"[bold green]Running {cmd}...[/bold green]", spinner="dots"): proc = subprocess.run( cmd, shell=True, - evalview/core/security.py:34mcp-python-ssrf-metadata-endpoint (critical/medium)
The cloud instance-metadata endpoint is contacted directly. On a hosted MCP server this is the canonical path from SSRF to credential theft: 169.254.169.254 serves IAM role credentials to anything that asks.
ipaddress.ip_network("fe80::/10"), # Cloud metadata endpoints ipaddress.ip_network("169.254.169.254/32"), # AWS/GCP/Azure metadata # Localhost IPv6 ipaddress.ip_network("::ffff:127.0.0.0/104"), - evalview/core/security.py:46mcp-python-ssrf-metadata-endpoint (critical/medium)
The cloud instance-metadata endpoint is contacted directly. On a hosted MCP server this is the canonical path from SSRF to credential theft: 169.254.169.254 serves IAM role credentials to anything that asks.
"localhost", "localhost.localdomain", "metadata.google.internal", "metadata.goog", "kubernetes.default.svc", - evalview/openclaw.py:107mcp-python-shell-true-nonliteral (critical/medium)
A subprocess is spawned with shell=True on a value that is not a string literal. Any caller-supplied component of that value is shell-injectable: `; rm -rf ~` in an argument is executed, not passed.
) try: subprocess.run(revert_cmd, shell=True, check=True, capture_output=True) except subprocess.CalledProcessError as e: logger.warning(f"Revert command failed: {e}") - evalview/openclaw.py:158mcp-python-shell-true-nonliteral (critical/medium)
A subprocess is spawned with shell=True on a value that is not a string literal. Any caller-supplied component of that value is shell-injectable: `; rm -rf ~` in an argument is executed, not passed.
cmd = revert_cmd or "git checkout -- ." try: subprocess.run(cmd, shell=True, check=True, capture_output=True) reverted = True except subprocess.CalledProcessError: - evalview/skills/evaluators/deterministic.py:776mcp-python-shell-true-nonliteral (critical/medium)
A subprocess is spawned with shell=True on a value that is not a string literal. Any caller-supplied component of that value is shell-injectable: `; rm -rf ~` in an argument is executed, not passed.
try: result = subprocess.run( command, shell=True, - evalview/skills/evaluators/deterministic.py:846mcp-python-shell-true-nonliteral (critical/medium)
A subprocess is spawned with shell=True on a value that is not a string literal. Any caller-supplied component of that value is shell-injectable: `; rm -rf ~` in an argument is executed, not passed.
else: # Simple foreground command result = subprocess.run( smoke_test.command, shell=True, - evalview/skills/evaluators/deterministic.py:903mcp-python-shell-true-nonliteral (critical/medium)
A subprocess is spawned with shell=True on a value that is not a string literal. Any caller-supplied component of that value is shell-injectable: `; rm -rf ~` in an argument is executed, not passed.
try: # Start background process process = subprocess.Popen( smoke_test.command, shell=True, - evalview/skills/evaluators/deterministic.py:1003mcp-python-shell-true-nonliteral (critical/medium)
A subprocess is spawned with shell=True on a value that is not a string literal. Any caller-supplied component of that value is shell-injectable: `; rm -rf ~` in an argument is executed, not passed.
if smoke_test.cleanup: try: subprocess.run( smoke_test.cleanup, shell=True, - evalview/commands/capture_cmd.py:321mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
import time as _time _req_start = _time.monotonic() resp = httpx.post( agent_url, content=body, - evalview/commands/gym_cmd.py:135mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
# Check if endpoint is reachable try: httpx.get(f"{endpoint.rstrip('/')}/health", timeout=5.0) console.print("[green]✓ Agent endpoint reachable[/green]\n") except Exception: - evalview/commands/init_cmd.py:124mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
def _probe(query: str) -> Optional[Dict[str, Any]]: try: r = httpx.post(endpoint, json={"query": query}, timeout=30.0) r.raise_for_status() return r.json() - evalview/commands/init_cmd.py:832mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
console.print("[dim]Probing agent to detect profile...[/dim]") try: probe_resp = httpx.post( endpoint, json={"query": "Hello, what can you help me with?"}, - evalview/commands/init_cmd.py:937mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
console.print(f"\n[dim]Checking agent at {endpoint}...[/dim]") try: _preflight = _httpx.post(endpoint, json={"query": "ping"}, timeout=5.0) if _preflight.status_code != 200: try: - evalview/skills/evaluators/deterministic.py:933mcp-python-ssrf-nonliteral-url (medium/low)
An outbound HTTP request is made to a URL that is not a literal. If any part of it is caller-supplied, the server can be steered at internal hosts — cloud metadata endpoints, admin ports, other tenants.
try: import urllib.request response = urllib.request.urlopen( smoke_test.health_check, timeout=2 )
Auth Posture
- authentication_model
stdio transport: the client launches the server as a subprocess, so the OS process boundary is the auth boundary — `03` §4 does not penalise absent auth here
- transport_security
declared transport: stdio — launched as a subprocess by the client, so the security boundary is the client process, not a network port (`03` §4)
- secret_handling
detect-secrets matched 20 credential-shaped string(s), every one from a keyword or entropy heuristic (Base64 High Entropy String, Secret Keyword). Those flag placeholders, variable names and content hashes as readily as credentials, so they do not establish the committed credential `03` §4's 0 asserts, and this sub-check is not scored; Secret Keyword at evalview/chat.py:580; Base64 High Entropy String at evalview/cloud/client.py:13; Secret Keyword at evalview/commands/init_cmd.py:1074; Secret Keyword at evalview/core/llm_configs.py:58; Secret Keyword at evalview/core/llm_configs.py:65; Secret Keyword at evalview/core/llm_configs.py:72; Secret Keyword at evalview/core/llm_configs.py:79; Secret Keyword at evalview/core/llm_configs.py:86; Secret Keyword at evalview/core/llm_configs.py:93; Secret Keyword at evalview/core/llm_configs.py:100; …and 10 more matches
- authorization_granularity
no tool registration was recognised anywhere in the source. An MCP server registers tools by definition, so this is a gap in our detection rather than a server with none, and `03` §4's threshold cannot be applied to it. A tool-list handler IS present, so the tools are built in a shape this counter could not enumerate.
Maintenance
- recency
last commit 41 day(s) before 2026-10-16
- issue_responsiveness
only 2 issue(s) from outside the project fall inside `03` §5's 6-month window, which is under §5's floor of 3, and §5's lifetime-median fallback needs this repository's whole issue history — more of it than `04` §7's bounded fetch reads. A limit of ours, not a finding about the server.
- release_cadence
median release gap 4 days (by releases), last release 82 day(s) ago
- bus_factor
this repository's 12-month history is longer than the bounded walk `04` §7 performs, and the authors found so far span more than one of `03` §5's bands. A limit of ours, not a finding about the server.
- repository_signals
`03` §5 scores this by quartile WITHIN the server's category, which needs a distribution across every scanned server and so cannot be computed while scanning one. It is a post-pass over the crawl and that pass does not exist yet.
Transparency
- license
LICENSE present with no SPDX-License-Identifier tag; identified as 'Apache-2.0' from its text
- readme_quality
README.md: installation instructions (+25)
- declared_scopes
neither the README nor the MCP manifest documents what the server accesses or what side effects it has
- changelog
no CHANGELOG in the fetched source. `03` §7's partial credit for tagged releases carrying release notes is not applied here — that needs the forge API, which this check does not call
- security_contact
no SECURITY.md and no disclosure contact in the README, so a finder has no route to report a vulnerability privately
Contest a finding
Every number here comes from a rule reading published source, and rules get things wrong. If something on this page is incorrect — or correct but mitigated by a check the rule cannot see — open an issue and we answer it in writing. A finding we got wrong is removed and the rule is fixed; one that stands is answered on the record.
Anyone can file: the maintainer, a researcher who disagrees with the classification, or a user who tested the server and could not reproduce it. Name this server and the rule or advisory ID shown beside the finding above. We aim to reply within five business days and to resolve within ten.