T09 · Insecure Skill Coding Practices
- Location
scripts/screenshot.py:33- Finding
Unrestricted Browser Navigation Enables Internal Network and Local File Access
- Content
View full analysis
Vulnerability Details
File Location:
scripts/screenshot.py, lines 33–39 and 57
Vulnerability Type: Unrestricted URL and URI navigation
Risk Level: MediumVulnerable Code
python html_path = args.html if not html_path.startswith(("http://", "https://", "file://")): abs_path = os.path.abspath(html_path) if not os.path.exists(abs_path): print(f"Error: File not found: {abs_path}", file=sys.stderr) sys.exit(1) html_path = f"file://{abs_path}" # ... page.goto(html_path, wait_until="networkidle")Technical Analysis
The renderer describes its input as a path to an HTML file, but it explicitly accepts arbitrary
http://,https://, andfile://URIs. Inputs using these schemes bypass the existence check and are passed directly to Playwright'spage.goto().Because Chromium runs with the host's network and filesystem visibility, an attacker who can control the input argument can direct it to:
- Loopback services such as
http://127.0.0.1:<port>/ - Private-network services
- Cloud instance metadata endpoints
- Sensitive files available through
file://URIs
This creates an SSRF-like browser navigation primitive and exceeds the access required to render a local diagram. Successful responses can be captured in the generated screenshot.
Attack Path
- An attacker gains influence over the value passed as the script's
htmlargument. - The attacker supplies an internal URL, metadata endpoint, or sensitive
file://URI. - The scheme check accepts the input without host, path, or origin validation.
- Headless Chromium navigates to the target using the host's network and filesystem access.
- The rendered response is saved to the attacker-selected or default PNG output.
- The screenshot may then be disclosed through the Skill's normal media-delivery workflow.
Impact Assessment
A successful exploit can expose content from HTTP services reachable only from the rendering host or from local fi ...[truncated 385 chars]
- Loopback services such as
- Remediation
View remediation
Remediation Suggestions
- Accept only local HTML files and reject all URI schemes, including
http://,https://, and caller-suppliedfile://values. - Resolve the input with
pathlib.Path.resolve()and verify that it is a regular file with an.htmlor.htmextension. - Restrict resolved inputs to a designated workspace using
Path.is_relative_to()or an equivalent canonical-path containment check. - Construct the file URI internally with
Path.as_uri()rather than accepting one from the caller. - Use Playwright request routing to abort all non-
file:requests during rendering:
python page.route( "**/*", lambda route: route.continue_() if route.request.url.startswith("file:") else route.abort(), )- Bundle fonts and other assets locally so network access can be disabled without affecting output.
- Store output only under an approved directory and validate its canonical path if untrusted callers can control the output argument.
A hardened input flow could use:
python from pathlib import Path workspace = Path.cwd().resolve() input_path = Path(args.html).resolve() if ( not input_path.is_file() or input_path.suffix.lower() not in {".html", ".htm"} or not input_path.is_relative_to(workspace) ): parser.error("Input must be a local HTML file inside the workspace") html_path = input_path.as_uri()- Accept only local HTML files and reject all URI schemes, including
