Skip to content

[Security] browser_screenshot writes to any path and creates directories outside the session roots #293

Description

@Mr-Neutr0n

Sibling of #189 (browser_upload_file), same root cause, opposite direction: that one is an arbitrary read, this is an arbitrary write.

The code

coworker/connectors/browser_automation.py:

def browser_screenshot(path: str = "") -> dict[str, Any]:
    def run(page):
        out = (Path(path).expanduser() if path
               else Path(tempfile.gettempdir()) / "coworker-browser-screenshot.png")
        out = out.resolve()
        out.parent.mkdir(parents=True, exist_ok=True)
        page.screenshot(path=str(out), full_page=True)

No containment. out.parent.mkdir(parents=True) then a write, anywhere the process can reach.

Why the permission engine doesn't catch it

PermissionEngine.evaluate path-scopes only when is_write, and is_write is RiskClass.WRITE_LOCAL, which classify() assigns by name from a fixed set:

WRITE_TOOLS = {"write_file", "replace_in_file", "apply_patch", "apply_unified_diff"}

Connector tools resolve to EXTERNAL or READ, never WRITE_LOCAL, so the roots check never runs for them. The scoping is also keyed to arguments["path"] specifically, so any write tool naming its argument differently is invisible too.

Impact

Arbitrary file overwrite (destroying ~/.ssh/id_rsa, config files) and arbitrary directory-tree creation. Content is a PNG, so this is destructive rather than a direct code-execution primitive.

Approval-gated in Interactive mode, so not a silent compromise there — but in Auto mode there's no prompt, and even with one the tool reaches files the user never granted the session, which is what the roots list exists to prevent. The mkdir also happened before any error, so a refused call still left directories behind.

Suggested fix

make_integration_tools already receives roots, with the comment:

Email needs the session roots: attachment downloads land in the primary scratch and outgoing attachments must resolve inside a granted directory.

The browser tools just aren't passed them. Reuse the confinement email_tools.py:640 already applies, requiring a writable root for the screenshot write.

PR #289 does this for both browser_screenshot and browser_upload_file (#189), and moves the check ahead of the browser call so a refusal no longer creates directories on its way to failing.


Found during a security review of the repo; drafted with AI assistance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions