Skip to content

fix(security): detect non-http schemes in shell-command SSRF scan#3252

Open
mohamed-elkholy95 wants to merge 1 commit intoHKUDS:nightlyfrom
mohamed-elkholy95:fix/ssrf-url-regex-nonhttp-schemes
Open

fix(security): detect non-http schemes in shell-command SSRF scan#3252
mohamed-elkholy95 wants to merge 1 commit intoHKUDS:nightlyfrom
mohamed-elkholy95:fix/ssrf-url-regex-nonhttp-schemes

Conversation

@mohamed-elkholy95
Copy link
Copy Markdown
Contributor

Summary

  • contains_internal_url (the scanner ExecTool uses to reject commands targeting internal resources) only matched https?:// URLs, so the following slipped through and actually ran:
    • curl file:///etc/passwd — local-file read
    • curl gopher://127.0.0.1:6379/_SET … — classic Redis SSRF
    • curl ftp://10.0.0.1/… — internal FTP access
    • curl dict://169.254.169.254/info — cloud-metadata probing
  • Broadens the URL regex to any scheme://… form and splits the "targets an internal host?" decision into its own helper so the check is scheme-agnostic. file:// is always treated as internal; everything else is judged by whether the hostname resolves into a blocked range.

Changes

  • nanobot/security/network.py — broaden _URL_RE to [a-z][a-z0-9+.\-]*://…; add _url_targets_internal helper; contains_internal_url delegates to it.
  • tests/security/test_security_network.py — 5 new cases covering file://, gopher://, ftp://, dict://, plus a regression test that git clone ssh://git@github.com/… against a public host is not blocked.

Backward compatibility

  • validate_url_target is unchanged — WebFetch still only accepts http(s) URLs.
  • The existing http(s) scan cases (curl http://169.254.169.254/…, wget http://localhost:8080/…) still block.
  • Public-host URLs over non-http schemes (e.g. ssh://git@github.com/…, https://example.com/…) still pass.

Test plan

  • uv run pytest tests/security/test_security_network.py — 25 pass
  • uv run pytest tests/security tests/tools tests/agent — 862 pass, 0 fail
  • uv run ruff check nanobot/security/network.py
  • End-to-end in a running container: confirmed ExecTool rejects the four bypass vectors and still allows curl https://example.com/.

Refs

@Re-bin Re-bin deleted the branch HKUDS:nightly April 19, 2026 16:22
@Re-bin Re-bin closed this Apr 19, 2026
@Re-bin Re-bin reopened this Apr 19, 2026
The shell-command scanner in ``contains_internal_url`` only matched http(s)
URLs, but ``curl`` and ``wget`` support many more schemes, several of which
have long histories as SSRF vectors:

  - ``file:///etc/passwd`` — local filesystem read
  - ``gopher://127.0.0.1:6379/_SET`` — the classic Redis SSRF
  - ``ftp://10.0.0.1/…`` — internal FTP access
  - ``dict://169.254.169.254/…`` — cloud-metadata probing

The old regex ``https?://…`` silently let those through, so a prompt-
injection-driven LLM emitting ``curl gopher://…`` cleared the exec guard
and actually ran.

This broadens the URL regex to any ``scheme://…`` form and moves the
internal-target decision off of ``validate_url_target`` (which conflates
"is this URL fetchable via http client?" with "does it target an internal
host?"). The new helper ``_url_targets_internal`` is scheme-agnostic:
``file://`` is always internal, anything else is judged by whether the
host resolves into a blocked range. Public hosts over non-http schemes
(e.g. ``git clone ssh://git@github.com/…``) are unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants