Skip to content

fix: harden the plugin probe, bundle lookup, explorer and trigger regexes against hostile input - #605

Merged
earakely-scale merged 5 commits into
mainfrom
edgararakelyan/local-surface-hardening
Nov 10, 2026
Merged

earakely-scale merged 5 commits into
mainfrom
edgararakelyan/local-surface-hardening

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Five small paths by which content the operator doesn't control reaches the operator's machine or the gateway:

  • The plugin probe ran modules from the current folder. plugin add and plugin remove read the environment with a fresh interpreter started as python -c, and preview with python -m pip. Both put the current folder first on the import path, so a packaging.py or pip/ in an untrusted checkout ran before any prompt, even with --dry-run.
  • A folder could stand in for an installed bundle. agent-env run NAME ran a folder called NAME in the current directory ahead of the installed bundle of that name.
  • The explorer API trusted every loopback port. Its CSRF guard refused only Sec-Fetch-Site: cross-site, but a page on another port of 127.0.0.1 is same-site, and a browser that sends no Fetch Metadata was trusted. The default CORS rule also let any loopback port read responses. The local providers publish env sites, pgweb and agent pages on loopback ports, so any of them could start or cancel runs and read the API.
  • The explorer's object proxy sandboxed only six types. An agent chooses an object's type and name, and Atom, RSS, XSL and multipart documents run script as HTML does, on the explorer's own origin.
  • A trigger's regex could stall the gateway. The gateway matched a trigger's regex with the standard library, with no time or size limit, on the event loop every caller shares. The gateway's control token already stops the agent registering a trigger, but a task author's pattern still runs against text the agent chooses.

What changes

  • Plugin probe: the snapshot probe, the resolver preview and every python -m pip the plugin commands run now start the interpreter with -P, as plugin show and the config probe already did. Relative specs such as ./plugin still resolve, since the working directory is unchanged.
  • Bundle lookup: a name that is both a folder here and an installed bundle, bare or as DIST/NAME, is refused, naming ./NAME for the folder and DIST/NAME for the bundle. A folder with no installed namesake still runs by its bare name. bundle init now prints its run command as a path (./triage). The CLI's note for a folder that wasn't a bundle could no longer be reached, so it's removed.
  • Explorer requests: /api admits a browser request only with Sec-Fetch-Site: same-origin or none, or, from a browser that sends no Fetch Metadata, an Origin equal to the explorer's own. A configured cors_origins entry is still admitted. The default CORS policy no longer lets another origin read responses. curl and the CLI send neither header and are unaffected, and the UI's dev server proxies /api through its own origin.
  • Explorer objects: only images (not SVG or any +xml type), audio, video, PDF, plain text, CSV and JSON are served unsandboxed, under script-src 'none'; frame-ancestors 'self'. Every other type gets sandbox; frame-ancestors 'self'.
  • Trigger regexes: matched with the regex package under a 0.1 s timeout, against at most the first million characters. A match that runs out matches nothing, and is logged. A where then doesn't fire, an unless doesn't cancel and a check step fails. Registration refuses a non-string regex or one longer than 4,096 characters. regex joins the gateway image's requirements and the framework's dev extra; outside the container only the tests import the engine.

Compatibility

  • A bare agent-env run NAME where NAME is also an installed bundle now fails with the two spellings to use. The integration fixture that avoided the name hello for that reason keeps working.
  • Anyone who pointed the explorer UI's dev build straight at :8234 from another origin has to list that origin in [explorer] cors_origins.
  • Opening an object of a non-passive type in the explorer shows it sandboxed: its script doesn't run.
  • The gateway image rebuilds with regex. regex keeps the standard library's syntax, so existing trigger patterns match as before.

Tests

  • Unit:
    • the snapshot probe runs no module from the current folder;
    • the pip commands carry -P;
    • a folder-and-bundle name is refused bare and as DIST/NAME, and runs spelled out;
    • bundle init prints a relative folder as ./NAME;
    • the explorer refuses cross-site, same-site and a foreign Origin, and admits same-origin, none, its own Origin and no headers;
    • no origin reads a response by default;
    • every type the ticket names is sandboxed and the passive ones get the scriptless policy;
    • a runaway regex returns within its budget and leaves its trigger armed;
    • registration refuses a long or non-string regex.
  • On an agent-environments-admin EC2 box:
    • the unit suite: 14,396 passed. Two failures were the plugin tests' fake matching commands by position, fixed, and the six changed test files rerun (508 passed). The other 5 also fail on main on that box;
    • the bundle-run CLI tests and the gateway's trigger and run tests, with the gateway image built from this source: 86 passed, and the 61 skips are declared capability gaps (remote sandbox).

Not covered here

  • The object proxy's policy wasn't checked in a real browser. PDF previews in Chromium and Firefox should still render under script-src 'none'.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; the previous test issue is fixed.

Summary

The PR hardens plugin commands, bundle lookup, explorer requests and object previews, and trigger regex matching.

  • Plugin checks and pip commands ignore modules in the current folder.
  • Bundle runs make colliding folder and installed names explicit.
  • The explorer API accepts browser requests only from allowed pages.
  • The explorer serves only known passive object types without a sandbox.
  • Gateway trigger regexes stop matching after fixed size and time limits.

Reviews (2) · Last reviewed commit: "test(gateway): a regex that runs out of ..." · Reviewed by Greptile

earakely-scale and others added 4 commits November 10, 2026 14:12
…their path

plugin add and remove read the environment with python -c and preview and install with python -m pip, which put the current folder first on the import path, so a module there named like one they import ran before any prompt, --dry-run included. They now start the interpreter with -P, as plugin show and the config probe already did; the working directory is unchanged, so relative specs still resolve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e is refused

agent-env run NAME ran a folder called NAME in the current directory ahead of the installed bundle of that name, so an untrusted checkout could stand in for it. Such a name, bare or as DIST/NAME, is now refused with the two spellings to use, ./NAME and DIST/NAME. A folder with no installed namesake still runs by its bare name. bundle init prints its run command as a path, and the CLI's note for a folder that wasn't a bundle, which could no longer be reached, is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… object type but the passive ones

The CSRF guard refused only Sec-Fetch-Site: cross-site, but every loopback port is one site and the local providers publish env sites, pgweb and agent pages there, and a browser sending no Fetch Metadata was trusted. /api now admits a browser request only from the same origin, a typed URL, or, with no Fetch Metadata, the explorer's own Origin; a configured cors_origin is still admitted, and curl and the CLI are unaffected. The default CORS policy no longer lets another origin read a response.

The object proxy sandboxed six types, while an agent chooses an object's type and name and Atom, RSS, XSL and multipart documents run script. Only images (not SVG or +xml), audio, video, PDF, plain text, CSV and JSON are served unsandboxed, under script-src 'none'; frame-ancestors 'self'; every other type gets sandbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gateway matched a trigger's regex with the standard library, unbounded, on the event loop every caller shares, against text the agent or an env server chose. It now uses the regex package with a 0.1 s timeout over at most the first million characters; a match that runs out matches nothing and is logged, so a where doesn't fire, an unless doesn't cancel and a check fails. Registration refuses a non-string regex or one over 4096 characters. regex joins the gateway image's requirements and the dev extra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​regex@​2026.11.9100100100100100

View full report

Comment thread tst/unit/env/gateway/triggers_test.py Resolved
…ut, not just to fail fast

The test let any quick non-match pass. It now stands in for regex.search, checks the budget it was given, raises TimeoutError, and asserts the trigger stays armed and the timeout is logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit e4a6b8e into main Nov 10, 2026
52 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/local-surface-hardening branch November 10, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant