Skip to content

security(browser): validate the URL in capture_screenshot (#13204) - #16544

Merged
mrveiss merged 2 commits into
mainfrom
fix-13204-playwright-screenshot-ssrf
Sep 13, 2026
Merged

mrveiss merged 2 commits into
mainfrom
fix-13204-playwright-screenshot-ssrf

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Thinking Path

#13204 lists four browser/fetch entry points and which ones run the DNS-resolving public-address guard before reaching the network. content_reach/backends/browser.py and research_browser_manager.py already did. api/browser_mcp.py::send_to_browser_vm did not at filing time, but that half was fixed separately (a7afad817, _reject_non_public_url + is_public_url_async) — its own docstring now documents the exemption for the admin surface that legitimately needs internal hosts. The remaining, unfixed half is services/playwright_service.py: capture_screenshot(url=...) posts a caller-supplied URL straight to the container's /test-frontend endpoint with no check at all.

Two callers reach it: browser_backends/container.py (the ADR-009 canonical-interface wrapper, whose own docstring says the registry validates the URL before dispatch — so callers arriving through get_browser() are already covered) and api/playwright.py's admin-gated /playwright/screenshot endpoint (calls it directly, bypassing the registry). Guarding inside capture_screenshot itself closes the gap for both paths at once — including any future direct caller — rather than only fixing the one HTTP endpoint, and it is a defense-in-depth check for the registry-routed path (a no-op there since that URL is already public by the time it arrives).

search_web and test_frontend/send_test_message were checked and excluded: search_web takes a query string, not a url — the container decides the destination, there is nothing here to validate. test_frontend/send_test_message default to ServiceURLs.FRONTEND_LOCAL and exist specifically to reach AutoBot's own internal frontend — guarding those would break their purpose, matching the same admin-surface exemption already documented in api/browser_mcp.py.

This does not attempt the larger ADR-009 migration (#13236 steps 5-6, routing tool_handler.py and api/playwright.py through get_browser() and retiring is_url_allowed) — that is real, separately-reviewable architectural work with its own production-behaviour-change implications. This PR is scoped to the one concrete, unguarded SSRF gap #13204 names.

What Changed

  • autobot-backend/services/playwright_service.py: capture_screenshot now calls autobot_shared.url_safety.is_public_url_async(url) before touching the network, returning {"success": False, "error": "URL is not a public address", "url": url} on rejection — same guard function and same shape of check api/browser_mcp.py::_reject_non_public_url already applies to send_to_browser_vm.
  • autobot-backend/services/playwright_service_test.py (new): asserts a non-public URL is rejected before is_ready() is even awaited, and that a public URL still reaches the container exactly as before (mocked http_client.post).
  • changelog/unreleased/13204-playwright-screenshot-ssrf-guard.md.

Verification

Pre-push hook's own pytest run on the changed file, from the actual push:

[pre-push] running pytest on:
    autobot-backend/services/playwright_service_test.py
[pre-push OK] pytest: all relevant tests pass

Mutation argument (not run locally): removing the is_public_url_async check restores the exact pre-fix behaviour that test_non_public_url_is_rejected_before_any_http_call asserts against — the function would fall straight into the is_ready()/http_client.post path for http://169.254.169.254/..., and the test's is_ready.assert_not_awaited() would fail.

Risks

Low. The added check runs before any I/O and only tightens behaviour for non-public URLs, which were never a supported use case for this caller-facing screenshot function. browser_backends/container.py callers see no behaviour change — the registry already validated their URL first, so the added check is a no-op pass-through for them.

Model Used

Claude Sonnet 5 (claude-sonnet-5)

Issue Link

Closes #13204

Changelog fragment

  • Added changelog/unreleased/13204-playwright-screenshot-ssrf-guard.md

Single-issue rationale

Single-issue rationale: this is the one concrete, unguarded call site #13204 names; #13236's remaining steps (5-6) are a separate architectural migration with their own review/rollout needs and are being tracked and scoped independently.

Checklist

  • Code follows AutoBot patterns from CLAUDE.md
  • Tests added or updated (or N/A with reason)
  • Documentation updated if behavior changed (docstring only; no user-facing doc)
  • Pre-commit hooks pass (git commit runs them automatically)
  • PR targets main — the default branch since the 2026-09-12 rename (this checklist item's "Dev_new_gui" wording is stale template text)
  • No secrets or credentials in the diff

🤖 Generated with Claude Code

send_to_browser_vm already runs a DNS-resolving public-address check
(a7afad8); services/playwright_service.py::capture_screenshot did
not, and posts a caller-supplied url straight to the container's
/test-frontend endpoint. Guard it with the same
autobot_shared.url_safety.is_public_url_async check.

test_frontend/send_test_message stay unguarded on purpose: their
frontend_url defaults to the internal frontend service and is meant
to reach it, matching the admin-surface exemption pattern already
documented in api/browser_mcp.py. search_web takes a query string,
not a url, so it was never an instance of this gap.
@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 79c7b784-ba52-4b82-afd7-490ecc89aecb


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@github-actions

Copy link
Copy Markdown
Contributor

Notice: 29 open PRs — past the runaway threshold (25)

There is no PR queue limit, and this is not a request to defer this PR. Work proceeds one issue at a time without a cap on open PRs; review capacity is the constraint.

This notice only means the count is high enough to be worth a glance for a runaway — something opening PRs in a loop, or a merge pipeline that has stalled so nothing is draining.

Currently open:

If the queue is draining normally, ignore this. Otherwise:

  1. Check whether CI is dispatching at all — see the ci-dispatch-watchdog status on these PRs
  2. Merge the ones whose CI has finished and review has passed: gh pr merge <number> --squash --delete-branch
  3. Look for a loop opening near-identical PRs

Warn-only runaway detector — .github/workflows/pr-queue-gate.yml. It never blocks a merge.

@github-actions

Copy link
Copy Markdown
Contributor

AutoBot Phase Validation Results

System Maturity: 96.5%

Phase Status:

PASS Phase 1: Core Infrastructure: 100.0%
PASS Phase 2: Knowledge Base and Memory: 100.0%
IN PROGRESS Phase 3: LLM Integration: 66.7%
PASS Phase 4: Security and Authentication: 100.0%
PASS Phase 5: Agent Orchestration: 100.0%
PASS Phase 6: Enhanced UI/UX: 100.0%
PASS Phase 7: Testing and Validation: 100.0%
PASS Phase 8: Advanced Features: 100.0%
PASS Phase 9: Multi-Modal AI: 100.0%
PASS Phase 10: Production Readiness: 100.0%

Recommendations:

  • 🟡 MEDIUM: Phase 3: LLM Integration requires attention (66.7% complete): Review and implement
  • ✅ System is production-ready - consider advanced features and scaling: Review and implement

@mrveiss

mrveiss commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Independent review at aa297f77c: approved.

Non-blocking:

@mrveiss
mrveiss force-pushed the fix-13204-playwright-screenshot-ssrf branch from aa297f7 to fc7c07a Compare September 12, 2026 20:03
@github-actions

Copy link
Copy Markdown
Contributor

AutoBot Phase Validation Results

System Maturity: 96.5%

Phase Status:

PASS Phase 1: Core Infrastructure: 100.0%
PASS Phase 2: Knowledge Base and Memory: 100.0%
IN PROGRESS Phase 3: LLM Integration: 66.7%
PASS Phase 4: Security and Authentication: 100.0%
PASS Phase 5: Agent Orchestration: 100.0%
PASS Phase 6: Enhanced UI/UX: 100.0%
PASS Phase 7: Testing and Validation: 100.0%
PASS Phase 8: Advanced Features: 100.0%
PASS Phase 9: Multi-Modal AI: 100.0%
PASS Phase 10: Production Readiness: 100.0%

Recommendations:

  • 🟡 MEDIUM: Phase 3: LLM Integration requires attention (66.7% complete): Review and implement
  • ✅ System is production-ready - consider advanced features and scaling: Review and implement

@mrveiss

mrveiss commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Delta aa297f77c..fc7c07adf: still approved. No content change. The net diff against main is identical to the approved aa297f77c: the same 3 files and 0 differing added or removed lines. The only new commit is the changelog fragment fc7c07adf (changelog/unreleased/13204-playwright-screenshot-ssrf-guard.md), which the approved net diff already contained. I did not re-check CI here.

@mrveiss
mrveiss merged commit 0abe0d4 into main Sep 13, 2026
74 checks passed
@mrveiss
mrveiss deleted the fix-13204-playwright-screenshot-ssrf branch September 13, 2026 06:39
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.

security(browser): send_to_browser_vm and playwright_service bypass the SSRF/DNS-rebind guard the other browser paths enforce

1 participant