Skip to content

Studio: harden auto permission network gates#7180

Open
shimmyshimmer wants to merge 2 commits into
mainfrom
agent/harden-auto-permission-network-gates
Open

Studio: harden auto permission network gates#7180
shimmyshimmer wants to merge 2 commits into
mainfrom
agent/harden-auto-permission-network-gates

Conversation

@shimmyshimmer

@shimmyshimmer shimmyshimmer commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

  • block low-level Python network clients that bypass the sandbox host scanner
  • classify those imports as unsafe in Approve for me
  • detect additional canvas network sinks, including remote form actions, resource attributes, setAttribute, template property access, and computed global properties
  • add regression coverage for each bypass

Why

This is a follow-up to #7079.

Installed clients such as httpcore, boto3, and botocore can open arbitrary endpoints without passing through the existing requests and urllib host checks. Canvas detection also missed equivalent network-capable forms such as window[`fetch`], setAttribute("src", ...), and remote form actions.

The sandbox now fails closed for those low-level clients, and Approve for me pauses before either the Python or canvas paths can make an unreviewed network request.

Validation

  • Ruff format and lint checks passed for all changed Python files
  • pytest studio/backend/tests/test_permission_mode.py studio/backend/tests/test_sandbox_tools.py -q -k 'not permission_mode_confirm_derivation and not confirm_gate_needs_stream'
  • 838 passed, 2 deselected

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request restricts low-level network modules (such as httpcore, boto3, and botocore) in sandboxed Python environments and expands static analysis regexes to detect additional HTML attributes, setAttribute calls, and bracket-access obfuscation on global host objects. The review feedback points out that the updated JavaScript bracket-access regex lacks word boundaries, which can lead to false positives on benign properties like window['is_open'] or window['fetching']. A code suggestion is provided to add word boundaries around the API names to resolve this issue.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread studio/backend/core/inference/tools.py Outdated
Comment on lines +2424 to +2426
r"\b(?:window|self|globalThis|top|parent|frames)\s*\[[^\]]*"
r"(?:fetch|open|XMLHttpRequest|WebSocket|EventSource|importScripts|"
r"sendBeacon|serviceWorker)[^\]]*\]|"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current regular expression for matching bracket access on global host objects uses [^\]]* around the API names (like fetch or open) without word boundaries. This causes severe false positives on benign properties or variables that contain these substrings (for example, window['is_open'], window['open_modal'], or window['fetching']), which are extremely common in frontend canvas code. Adding word boundaries (\b) around the API names prevents these false positives while still correctly catching obfuscated or direct bracket access.

Suggested change
r"\b(?:window|self|globalThis|top|parent|frames)\s*\[[^\]]*"
r"(?:fetch|open|XMLHttpRequest|WebSocket|EventSource|importScripts|"
r"sendBeacon|serviceWorker)[^\]]*\]|"
r"\b(?:window|self|globalThis|top|parent|frames)\s*\[[^\]]*"
r"\b(?:fetch|open|XMLHttpRequest|WebSocket|EventSource|importScripts|"
r"sendBeacon|serviceWorker)\b[^\]]*\]|"

@shimmyshimmer shimmyshimmer marked this pull request as ready for review July 16, 2026 13:37
@shimmyshimmer

Copy link
Copy Markdown
Member Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eed25b3845

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +5142 to +5144
def visit_Import(self, node):
for alias in node.names:
self._block_low_level_network_module(alias.name, node)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block dynamic imports of low-level clients

For snippets that dynamically import one of these clients, the sandbox check still returns safe because the new block only runs from visit_Import/visit_ImportFrom. For example, import importlib; boto3 = importlib.import_module('boto3'); boto3.client('s3').list_buckets() avoids the added import statement checks and boto3.client is not covered by _NETWORK_FQ_PREFIXES, so an installed low-level client can still make arbitrary network calls outside the host allowlist. Please also reject literal dynamic imports such as importlib.import_module(...) and __import__(...) for these module roots.

Useful? React with 👍 / 👎.

Comment on lines +2411 to +2412
r"\b(?:src|href|srcset|action|formaction|poster|data|ping)\s*="
r"\s*[\"']?\s*(?:https?:|/)|"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scan every URL in ping attributes

When an anchor ping value starts with a bare relative URL, this pattern only checks the first token after = and misses later remote ping targets; e.g. <a ping='local https://evil/x'> remains auto-approved even though ping is a space-separated URL list and the browser will POST to the remote URL on click. Since this change adds ping to the network gate, the full attribute value needs to be scanned for any http(s): or root-relative entry rather than only the first URL.

Useful? React with 👍 / 👎.

Comment thread studio/backend/core/inference/tools.py Outdated
Comment on lines +2424 to +2426
r"\b(?:window|self|globalThis|top|parent|frames)\s*\[[^\]]*"
r"(?:fetch|open|XMLHttpRequest|WebSocket|EventSource|importScripts|"
r"sendBeacon|serviceWorker)[^\]]*\]|"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect split template-literal global keys

For a canvas that invokes fetch through a split template key, such as window[`fet`+`ch`]('https://evil'), this new branch does not match because it only looks for a contiguous fetch/open token inside the brackets, and the computed-key rule below still only recognizes single/double quotes around +. Since this change now supports template-literal property access for the direct form, the same trivial split can still auto-run a networked canvas without approval.

Useful? React with 👍 / 👎.

Comment thread studio/backend/core/inference/tools.py Outdated
Comment on lines +2413 to +2414
r"\.\s*setAttribute\s*\(\s*[\"'`](?:src|href|srcset|action|formaction|"
r"poster|data|ping)[\"'`]|"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Require a network URL for setAttribute

For static canvases that build DOM nodes programmatically, e.g. <script>img.setAttribute('src','./local.png')</script>, this branch now asks for approval even though the value is only a relative local resource, because it checks only the attribute name and ignores the second argument. That regresses the documented behavior where static/relative-resource canvases auto-run; require the assigned value to be http(s) or root-relative before pausing.

Useful? React with 👍 / 👎.

Comment on lines +2411 to +2412
r"\b(?:src|href|srcset|action|formaction|poster|data|ping)\s*="
r"\s*[\"']?\s*(?:https?:|/)|"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Avoid matching plain data assignments

Because this regex runs over the whole HTML/JS string rather than only parsed attributes, adding data makes common static script state like <script>const data = '/tmp/file.json'</script> require approval even when nothing loads from the network. That regresses the static-canvas auto-run path for ordinary variables named data; constrain this branch to actual HTML attributes or otherwise avoid matching JavaScript assignments.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec26768cbb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +5252 to +5261
def _is_import_loader(self, node) -> bool:
if isinstance(node, ast.Name):
return node.id in self.import_loader_aliases
if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name):
return False
if node.attr == "import_module":
return node.value.id in self.importlib_aliases
if node.attr == "__import__":
return node.value.id in self.builtins_aliases
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve import loaders hidden behind dynamic lookup

When sandboxed Python obtains the loader with a dynamic lookup, this check never sees an import loader because node.func is an ast.Call/ast.Subscript rather than a Name or Attribute. For example, import importlib; boto3 = getattr(importlib, "import_module")("boto3"); boto3.client("s3").list_buckets() still bypasses the new low-level module block, and boto3.client is not covered by the host allowlist scanner, so arbitrary network access can run after approval. Please treat literal getattr/namespace lookups of import_module or __import__ as import loaders too.

Useful? React with 👍 / 👎.

Comment on lines +2433 to +2437
_RENDER_HTML_SET_ATTRIBUTE_RE = re.compile(
r"""\.\s*setAttribute\s*\(\s*
(?P<quote>["'`])
(?P<attr>src|href|srcset|action|formaction|poster|data|ping)
(?P=quote)\s*,\s*(?P<value>[^)]*)\)""",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include optional calls in setAttribute scanning

For canvases that use optional chaining, such as <script>img.setAttribute?.('src','https://evil/x')</script>, this pattern does not match because it requires setAttribute( immediately after optional whitespace. Since the generic URL regex does not flag a bare https:// string, that canvas remains auto-approved even though the browser still sets the network-loading attribute when setAttribute exists. Please allow the optional-call token (?.) in this sink check.

Useful? React with 👍 / 👎.

Comment on lines 5321 to +5325
def visit_Call(self, node):
if node.args and self._is_import_loader(node.func):
module_name = self._static_string(node.args[0])
if module_name is not None:
self._block_low_level_network_module(module_name, node)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Check keyword module names in dynamic imports

When the dynamic import loader is called with the name keyword, this block is skipped because it only inspects positional node.args. For example, import importlib; boto3 = importlib.import_module(name='boto3'); boto3.client('s3').list_buckets() is not rejected by the sandbox, and boto3.client is still outside _NETWORK_FQ_PREFIXES, so the low-level client can make arbitrary network calls after approval. Please inspect the literal name= keyword for these import loaders as well.

Useful? React with 👍 / 👎.

Comment on lines +2429 to +2431
_RENDER_HTML_GLOBAL_BRACKET_RE = re.compile(
r"\b(?:window|self|globalThis|top|parent|frames)\s*\[([^\]]*)\]",
re.IGNORECASE | re.DOTALL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match optional chaining before global bracket access

For canvases that optional-chain the global before a computed key, such as <script>window?.['fetch']('https://evil/x')</script>, this regex no longer sees the window[...] access because of the intervening ?., and the generic fetch( pattern does not match fetch'](. That means a networked canvas can still auto-run without approval; please allow the optional-chain token before the bracket for the global hosts scanned here.

Useful? React with 👍 / 👎.

value = _static_js_string(match.group("value"))
if value is None:
return True
if value.lstrip().lower().startswith(("http:", "https:", "/")):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scan URL-list values passed to setAttribute

When a programmatic setAttribute writes a URL-list attribute whose first entry is local, this check approves because it only tests the start of the static value. For example, <script>img.setAttribute('srcset','local.png 1x, https://evil/x.png 2x')</script> can still load the remote candidate, and ping has the same space-separated URL-list shape. Please scan the full value for remote or root-relative entries for URL-list attributes.

Useful? React with 👍 / 👎.

Comment on lines +2435 to +2437
(?P<quote>["'`])
(?P<attr>src|href|srcset|action|formaction|poster|data|ping)
(?P=quote)\s*,\s*(?P<value>[^)]*)\)""",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fold setAttribute names before approving canvases

When the attribute name is built from literal pieces, e.g. <script>img.setAttribute('s'+'rc','https://evil/x.png')</script>, this regex does not match because it requires a single quoted attribute token. The generic URL scanner does not flag a bare https:// string, so the canvas auto-runs even though the browser assigns a network-loading src. Please fold static attribute-name expressions before treating the call as safe.

Useful? React with 👍 / 👎.

Comment on lines +2429 to +2431
_RENDER_HTML_GLOBAL_BRACKET_RE = re.compile(
r"\b(?:window|self|globalThis|top|parent|frames)\s*\[([^\]]*)\]",
re.IGNORECASE | re.DOTALL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat top-level this as a global bracket host

In a classic canvas <script>, top-level this is window, but the new host-object regex only matches named globals. Thus <script>this['fetch']('https://evil/x')</script> is now auto-approved because the direct fetch( pattern does not see the quoted property, although it issues the same network request. Please include this or retain the literal bracket-member detection for this case.

Useful? React with 👍 / 👎.

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.

1 participant