Back to skill

Security audit

Steel Browser

Security checks for vulnerabilities and agentic risk

Overview

The skill is coherent browser automation tooling, but its shell helpers can be tricked by crafted inputs into running local Python code, so it belongs in Review before installation.

Install only after reviewing or fixing the heredoc argument-interpolation issue. Until fixed, run it in an isolated environment, avoid passing selectors, URLs, text, output paths, or JavaScript derived from untrusted webpages, avoid sensitive logged-in sessions, pin dependencies, and protect or remove ~/.steel_state after use.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/click.sh:15
Finding

Arbitrary Local Python Code Execution Through Unquoted Heredoc Argument Interpolation

Content
View full analysis

Vulnerability Details

File Location: scripts/click.sh:15-24
Additional Affected Locations: scripts/click_coords.sh:22-31, scripts/eval_js.sh:13-22, scripts/get_content.sh:21-36, scripts/hover.sh:10-19, scripts/navigate.sh:12-22, scripts/press_key.sh:16-25, scripts/screenshot.sh:20-30, scripts/scroll.sh:12-35, scripts/select.sh:12-23, scripts/start_session.sh:25-55, scripts/type.sh:12-21, and scripts/wait_for.sh:12-21
Vulnerability Type: Python source-code injection
Risk Level: High

Vulnerable Code

A representative vulnerable implementation is:

bash
python3 - <<PYEOF
import os, sys
HELPER = "$SCRIPT_DIR/_connect.py"
exec(open(HELPER).read())

try:
    page.click("""$SELECTOR""")
    print(f"Clicked: $SELECTOR")
finally:
    cleanup()
PYEOF

The same unsafe pattern is used for other caller-controlled values. Representative examples include:

bash
page.goto("$URL", wait_until="$WAIT_UNTIL")
bash
result = page.evaluate("""$JS""")
bash
page.fill("""$SELECTOR""", """$TEXT""")
bash
page.screenshot(path="$OUTPUT", full_page=full_page)
bash
page.wait_for_selector("""$SELECTOR""", timeout=int("$TIMEOUT_MS"))

Technical Analysis

The scripts construct a Python program in an unquoted shell heredoc and directly interpolate command-line arguments into Python string literals. Triple-quoted strings do not safely serialize arbitrary input. A value containing a closing triple quote can terminate the intended literal and append new Python statements.

For example, a malicious selector shaped like the following can break out of the generated string:

text
x"""); __import__("os").system("id"); #

It causes the generated Python source to contain an attacker-controlled statement conceptually equivalent to:

python
page.click("""x""")
__import__("os").system("id")

The inje ...[truncated 2215 chars]

Remediation
View remediation

Remediation Suggestions

Do not generate Python source code by interpolating shell arguments. Pass each value as a normal process argument and use a single-quoted heredoc so the shell cannot expand its contents:

bash
python3 - "$SCRIPT_DIR/_connect.py" "$SELECTOR" <<'PY'
import sys

helper = sys.argv[1]
selector = sys.argv[2]
exec(compile(open(helper, encoding="utf-8").read(), helper, "exec"))

try:
    page.click(selector)
    print(f"Clicked: {selector}")
finally:
    cleanup()
PY

Apply the same design to every affected script:

  1. Pass selectors, URLs, text, JavaScript, paths, keys, and option values through sys.argv or a safely encoded data channel such as JSON.
  2. Quote heredoc delimiters as <<'PY'.
  3. Validate numeric fields in Bash and Python before use.
  4. Restrict enumerated values such as mouse buttons and page load states to explicit allowlists.
  5. Validate navigation schemes and screenshot destinations according to the intended trust boundary.
  6. Add regression tests containing quotes, triple quotes, newlines, backslashes, Unicode, and attempted Python statements.
  7. Prefer a shared Python command-line program using argparse over separate shell-generated Python fragments.

T08 · Insecure Dependencies

Warning
Location
SKILL.md:13
Finding

Unpinned Third-Party Dependencies Permit Mutable Supply-Chain Resolution

Content
View full analysis

Vulnerability Details

File Location: SKILL.md:13-16
Vulnerability Type: Unpinned runtime dependencies
Risk Level: Medium

Vulnerable Code

bash
pip install steel-sdk playwright
export STEEL_API_KEY=your_key_here

Technical Analysis

The documented installation command installs steel-sdk and playwright without exact versions, hashes, a lock file, or an explicitly trusted package index. Consequently, the code installed at deployment time can differ from the code reviewed during the audit.

Python package installation can process package-controlled metadata, build backends, and installation artifacts. A compromised upstream release, compromised package-index account, unsafe index configuration, or unexpectedly incompatible future release could therefore introduce code that executes during installation or later when imported by the Skill.

No evidence was found that the currently named packages are malicious. The issue is the absence of controls that bind installation to reviewed artifacts.

Attack Path

  1. A user follows the prerequisite command in SKILL.md.
  2. pip resolves the latest acceptable releases from its configured package index or indexes.
  3. A dependency release has changed since the Skill was reviewed, or the configured source serves a compromised artifact.
  4. The package is installed and its code is subsequently imported by _connect.py or another script.
  5. Malicious package code executes with the installing or invoking user's privileges and can access the Steel API key and local files.

Impact Assessment

A compromised dependency could execute arbitrary code with the privileges of the user performing installation or running the Skill. Depending on how installation is performed, this may expose:

  • The local account's files and environment variables.
  • STEEL_API_KEY and active Steel session identifiers.
  • Browser-session data and automation results.
  • Sy ...[truncated 223 chars]
Remediation
View remediation

Remediation Suggestions

  1. Pin direct and transitive dependencies to reviewed versions.

  2. Maintain a lock file or hashed requirements file.

  3. Install with hash verification, for example:

    bash
    python -m pip install --require-hashes -r requirements.txt
    
  4. Configure and document the intended HTTPS package index rather than relying on ambient pip configuration.

  5. Install dependencies in a dedicated virtual environment under an unprivileged account.

  6. Use dependency scanning and controlled update reviews before changing pinned versions.

  7. Avoid elevated installation unless it is operationally necessary.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/start_session.sh:48
Finding

Active Session Identifier Is Stored Without Explicit Restrictive File Permissions

Content
View full analysis

Vulnerability Details

File Location: scripts/start_session.sh:48-51
Vulnerability Type: Insecure sensitive state-file creation
Risk Level: Low

Vulnerable Code

python
state = {"session_id": str(session.id)}
state_path = os.path.expanduser("~/.steel_state")
with open(state_path, "w") as f:
    json.dump(state, f)

Technical Analysis

The active Steel session identifier is written using Python's ordinary open() function without explicitly enforcing owner-only permissions. The resulting mode is determined by the process umask. Under a permissive or misconfigured umask, another local user may be able to read the state file.

The code also does not explicitly protect creation against symbolic-link substitution. In a normally protected home directory, exploitation by another user may be constrained, but the implementation does not itself enforce that assumption.

The session ID is not sufficient by itself to establish the CDP connection shown in _connect.py, because the Steel API key is also required. Nevertheless, disclosure gives an attacker a valid identifier for an active session and increases the impact of any separately acquired API access.

Attack Path

  1. The Skill is run in an environment with a permissive umask or inadequately protected home directory.
  2. start_session.sh creates ~/.steel_state with permissions that permit another local principal to read it.
  3. The local principal obtains the active Steel session ID.
  4. If that principal also obtains usable Steel API credentials, it can attempt to connect to or release the identified session.
  5. Depending on session contents, this may expose browser data or disrupt active automation.

Impact Assessment

The directly exposed data is the active session identifier. Potential consequences include:

  • Disclosure of an identifier associated with a live cloud-browser session.
  • Easier targeting of the session if the API ...[truncated 277 chars]
Remediation
View remediation

Remediation Suggestions

Create the state file atomically with owner-only permissions and reject symbolic links:

python
import json
import os
import stat

state_path = os.path.expanduser("~/.steel_state")
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
if hasattr(os, "O_NOFOLLOW"):
    flags |= os.O_NOFOLLOW

fd = os.open(state_path, flags, 0o600)
try:
    os.fchmod(fd, stat.S_IRUSR | stat.S_IWUSR)
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        json.dump(state, f)
    fd = None
finally:
    if fd is not None:
        os.close(fd)

Additional hardening should include:

  1. Store state in an owner-only directory with mode 0700.
  2. Verify that an existing path is a regular file owned by the current user.
  3. Avoid following symbolic links.
  4. Remove stale state after session release or failed session creation.
  5. Avoid printing session identifiers or viewer URLs into logs that may be accessible to other users.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (7)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill exposes capabilities that rely on environment variables and persistent local state, but it does not declare any tool scope or permissions metadata. That makes the capability boundary implicit rather than explicit, increasing the chance an agent or user invokes the skill without understanding it can read secrets from env and write session state to disk.

Content

No source excerpt is available for this finding.

Context Window Stuffing

Medium
Category
Memory Poisoning
Confidence
85% confidence
Finding

Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Content

Scanner excerpt · SKILL.md (reported line 43)May include surrounding context.

md
| `screenshot.sh` | `[OUTPUT.png] [--full-page]` | Take screenshot |
| `click.sh` | `SELECTOR` | Click by CSS/text/aria selector |
| `click_coords.sh` | `X Y [--right] [--double]` | Click at pixel coords (fallback) |
| `type.sh` | `SELECTOR "text"` | Fill input field |
| `press_key.sh` | `KEY` | Press key (e.g. `Enter`, `Control+a`) |
| `scroll.sh` | `AMOUNT\|--to-bottom\|--to-top\|SELECTOR` | Scroll page |
| `hover.sh` | `SELECTOR` | Hover over element |

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
86% confidence
Finding

This code initiates a network connection to a remote browser session and transmits the API key and session ID as part of the connection URL. While the module docstring describes its purpose, there is no runtime confirmation, warning, or user-facing notice about the outbound connection or credential use.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The manifest emphasizes that this skill uses Playwright selectors instead of pixel coordinates for reliability in pure web tasks. This script explicitly performs clicks by raw screen coordinates as a fallback, which is a different interaction model than the one the manifest claims as the skill's operating approach.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

This script takes arbitrary JavaScript from a shell argument and passes it directly into page.evaluate, which executes in the context of the active browser page. In a browser-automation skill, that creates a powerful code-execution primitive against any site the agent is logged into, enabling DOM manipulation, data extraction, or triggering sensitive actions without any guardrails, confirmation, or policy checks.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/start_session.sh (reported line 2)May include surrounding context.

sh
#!/usr/bin/env bash
# start_session.sh - Create a new Steel browser session
# Usage: start_session.sh [--proxy] [--captcha] [--timeout MS] [--ua USER_AGENT]
#
# Saves session ID to ~/.steel_state for use by other scripts.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

The skill persists browser session identifiers in ~/.steel_state but does not warn users that session state is stored locally across invocations. Persistent session artifacts can enable unintended session reuse, confusion between tasks, or exposure of active browser session identifiers to other local processes or users.

Content

No source excerpt is available for this finding.