Back to skill

Security audit

Browserless Agent

Security checks for vulnerabilities and agentic risk

Overview

This browser automation skill does what it advertises, but it exposes powerful web, file, cookie, and secret-handling capabilities without enough containment or truthful warnings.

Install only if you trust the publisher and can run it in a tightly limited environment. Use wss://, least-privilege Browserless tokens, a disposable browser/container, restricted network egress, and a dedicated output/upload directory. Avoid using it on authenticated or sensitive sites unless you are prepared for cookies, localStorage, form values, headers, screenshots, PDFs, and uploaded files to appear in action results or logs.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (6)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
main.py:213
Finding
Unrestricted Local File Access and Network Upload<![CDATA[ ## Vulnerability Details **File Location**: `main.py:213-216` **Vulnerability Type**: Unrestricted file upload from local filesystem **Risk Level**: High ### Vulnerable Code ```python async def upload_file(page: Page, selector: str, files: List[str]) -> Dict[str, Any]: """Upload file(s) to file input.""" await page.set_input_files(selector, files) return {"status": "success", "action": "upload_file", "selector": selector, "files": files} ``` ### Technical Analysis The `files` argument is controlled by the action caller and is passed directly to Playwright's `set_input_files`. The implementation does not normalize paths, restrict access to an approved upload directory, reject symbolic links, deny access to sensitive locations, or require confirmation before reading and uploading a file. Playwright reads each specified local file and transfers its contents through the Browserless session to the active website. While user-selected file upload is part of the declared browser automation functionality, granting access to every file readable by the Skill process exceeds the minimum privilege required. The response also returns the supplied local paths, potentially disclosing filesystem layout through Agent transcripts or logs. ### Attack Path 1. An attacker controls or influences the arguments supplied to the `upload_file` action. 2. The browser is directed to an attacker-controlled website containing a file input. 3. The attacker supplies a sensitive path, such as an environment file, private key, cloud credentials file, Agent configuration, or source file. 4. `set_input_files` reads the file using the privileges of the local Skill process. 5. The browser uploads the file through the Browserless session. 6. The attacker retrieves the file from the destination server. ### Impact Assessment Successful exploitation allows unauthorized reading and network exfiltration of any file accessible to the Skill process. Depending on deployment p ...[truncated 346 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict uploads to a dedicated, explicitly configured upload directory. 2. Resolve each path with `Path.resolve()` and verify that it remains inside the approved directory. 3. Reject absolute paths, traversal sequences, symbolic-link escapes, device files, sockets, and other non-regular files. 4. Maintain a denylist for sensitive filenames and directories, including `.env`, SSH keys, credential stores, Agent state, and cloud configuration. 5. Require explicit user confirmation that displays the resolved file paths before upload. 6. Apply file-size, file-count, and permitted-extension limits. 7. Avoid returning complete local paths in action responses. 8. Run the Skill under a dedicated account with minimal filesystem permissions. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
main.py:171
Finding
Sensitive Form Values, Headers, Cookies, and Evaluation Results Are Exposed<![CDATA[ ## Vulnerability Details **File Location**: `main.py:171-184`, `main.py:319-321`, `main.py:389-392`, `main.py:434-436` **Vulnerability Type**: Sensitive-data exposure through logs and action responses **Risk Level**: High ### Vulnerable Code ```python print(f"Typing '{text}' into selector: {selector}", file=sys.stderr) await page.wait_for_selector(selector, timeout=20000) if clear: await page.fill(selector, "") if delay > 0: await page.type(selector, text, delay=delay) else: await page.fill(selector, text) print(f"Successfully typed into {selector}.", file=sys.stderr) return {"status": "success", "action": "type_text", "selector": selector, "text": text} ``` ```python async def evaluate(page: Page, expression: str) -> Dict[str, Any]: """Execute JavaScript in page context.""" print(f"Evaluating JavaScript expression: {expression}", file=sys.stderr) result = await page.evaluate(expression) print(f"Evaluation result: {result}", file=sys.stderr) return {"status": "success", "action": "evaluate", "expression": expression, "result": result} ``` ```python await page.context.add_cookies([cookie]) return {"status": "success", "action": "set_cookie", "cookie": cookie} ``` ```python async def set_extra_headers(page: Page, headers: Dict[str, str]) -> Dict[str, Any]: """Set extra HTTP headers for all requests.""" await page.set_extra_http_headers(headers) return {"status": "success", "action": "set_extra_headers", "headers": headers} ``` ### Technical Analysis The Skill logs text entered through `type_text` verbatim and includes the same value in its response. This input can contain passwords, access tokens, personal information, payment data, or other confidential form values. The `set_extra_headers` action returns all supplied headers, including values such as `Authorization`, `Cookie`, or custom API-key headers. The `set_cookie` action similarly returns the complete cookie value. Arbitrary JavaScript eval ...[truncated 1220 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Never log the `text` argument supplied to typing actions. 2. Return only operation status and non-sensitive metadata after entering form values. 3. Redact headers whose names include `Authorization`, `Cookie`, `Token`, `Secret`, `API-Key`, or equivalent terms. 4. Do not return cookie values from `set_cookie`; return only the cookie name and operation status. 5. Disable logging of JavaScript expressions and evaluation results by default. 6. Add a centralized redaction function that recursively sanitizes dictionaries, lists, exceptions, and response objects. 7. Treat password-like selectors and fields as sensitive even if their values do not resemble tokens. 8. Ensure error handlers do not include secrets received from Playwright. 9. Add automated tests verifying that known secret markers never appear in standard output, standard error, or returned JSON. 10. Correct the documentation so its security claims accurately reflect implemented behavior. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
main.py:15
Finding
Browserless Authentication Token Can Be Sent over Plaintext WebSocket<![CDATA[ ## Vulnerability Details **File Location**: `main.py:15-30`, `main.py:646-650`; `validate_config.py:61-68`, `validate_config.py:94-97` **Vulnerability Type**: Plaintext transmission of authentication credentials **Risk Level**: High ### Vulnerable Code ```python def get_browserless_ws_url() -> Optional[str]: """Construct Browserless WebSocket URL from URL and optional token.""" if not BROWSERLESS_URL: return None url = BROWSERLESS_URL.rstrip('/') if '/playwright' not in url: url = f"{url}/playwright/chromium" if BROWSERLESS_TOKEN: separator = '&' if '?' in url else '?' url = f"{url}{separator}token={BROWSERLESS_TOKEN}" return url ``` ```python safe_url = ws_url.split('?')[0] if '?' in ws_url else ws_url print(f"Connecting to Browserless at: {safe_url}", file=sys.stderr) browser = await p.chromium.connect(ws_url, timeout=30000) ``` ```python if ws_url.startswith('wss://') or ws_url.startswith('ws://'): checks.append(("Protocol", True, "Valid WebSocket protocol")) else: checks.append(("Protocol", False, "Must start with wss:// or ws://")) ``` ```python if BROWSERLESS_URL and not BROWSERLESS_URL.startswith('wss://'): if 'localhost' not in BROWSERLESS_URL and '127.0.0.1' not in BROWSERLESS_URL: recommendations.append("⚠️ Consider using wss:// instead of ws:// for production") ``` ### Technical Analysis The Browserless token is appended to the connection URL as a query parameter. The implementation accepts both encrypted `wss://` and plaintext `ws://` endpoints, including remote hosts. The configuration validator treats `ws://` as valid and only emits a non-blocking recommendation for non-local endpoints. Consequently, a token, browser commands, form values, cookies, page content, and uploaded data can traverse the network without transport encryption. Embedding the token in the query string also increases exposure to URL logging by proxies, service diagnostics, and ...[truncated 924 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require `wss://` for every non-loopback Browserless endpoint. 2. Reject any `ws://` endpoint when `BROWSERLESS_TOKEN` is configured. 3. Permit plaintext WebSocket only for structurally validated loopback addresses such as `localhost`, `127.0.0.0/8`, or `::1`. 4. Parse endpoints with a URL parser rather than string-prefix and substring checks. 5. Make insecure configurations fatal rather than advisory. 6. Where supported, place authentication in a protected header instead of a query parameter. 7. Ensure connection errors and proxy logs redact all query parameters. 8. Add tests covering remote plaintext endpoints, user-info URL components, IPv6 loopback, malformed hosts, and deceptive names such as `localhost.attacker.example`. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
main.py:267
Finding
Caller-Controlled Screenshot and PDF Paths Permit Arbitrary File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `main.py:267-317` **Vulnerability Type**: Unrestricted filesystem write and overwrite **Risk Level**: Medium ### Vulnerable Code ```python async def screenshot(page: Page, url: Optional[str] = None, path: str = "screenshot.png", full_page: bool = False, selector: Optional[str] = None, quality: int = 90, type: str = "png") -> Dict[str, Any]: """Take screenshot of page or element.""" if url: await page.goto(url, timeout=30000, wait_until="domcontentloaded") await asyncio.sleep(2) print(f"Taking screenshot to {path} (full_page={full_page})", file=sys.stderr) screenshot_options = { "path": path, "full_page": full_page, "type": type } if type == "jpeg": screenshot_options["quality"] = quality if selector: element = page.locator(selector) await element.screenshot(**screenshot_options) else: await page.screenshot(**screenshot_options) print(f"Screenshot saved to {path}.", file=sys.stderr) return {"status": "success", "action": "screenshot", "path": path} ``` ```python async def pdf(page: Page, url: Optional[str] = None, path: str = "page.pdf", format: str = "A4", landscape: bool = False, margin: Optional[Dict] = None, print_background: bool = True) -> Dict[str, Any]: """Generate PDF from current page.""" if url: await page.goto(url, timeout=30000, wait_until="domcontentloaded") await asyncio.sleep(2) pdf_options = { "path": path, "format": format, "landscape": landscape, "print_background": print_background } if margin: pdf_options["margin"] = margin await page.pdf(**pdf_options) return {"status": "success", "action": "pdf", "path": path} ``` ### Technical Analysis Both actions pass caller-controlled paths directly to Playwright. There is no o ...[truncated 1246 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create a dedicated output directory with restrictive permissions. 2. Resolve output paths and verify containment within that directory. 3. Reject absolute paths, `..` traversal, symbolic-link escapes, and non-regular destinations. 4. Generate random or collision-resistant filenames server-side. 5. Use exclusive file creation where possible. 6. Require explicit confirmation before replacing an existing file. 7. Enforce expected extensions and MIME formats. 8. Return artifact identifiers or relative names instead of full filesystem paths. 9. Run the Skill with minimal write permissions outside the artifact directory. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
main.py:37
Finding
Unrestricted URL Navigation Enables Access to Internal and Metadata Services<![CDATA[ ## Vulnerability Details **File Location**: `main.py:37-42`, `main.py:58-60`, `main.py:623-624` **Vulnerability Type**: Server-side request forgery through remote browser navigation **Risk Level**: Medium ### Vulnerable Code ```python async def navigate(page: Page, url: str, wait_until: str = "domcontentloaded") -> Dict[str, Any]: """Navigate to a URL.""" print(f"Navigating to {url}", file=sys.stderr) await page.goto(url, timeout=30000, wait_until=wait_until) print(f"Navigation to {url} complete.", file=sys.stderr) return {"status": "success", "action": "navigate", "url": url, "final_url": page.url} ``` ```python async def get_text(page: Page, selector: str, url: Optional[str] = None, all: bool = False) -> Dict[str, Any]: """Extract text content from element(s).""" if url: await page.goto(url, timeout=30000, wait_until="domcontentloaded") ``` ```python if url: await page.goto(url) ``` ### Technical Analysis The Skill accepts arbitrary navigation targets without validating URL schemes, destination addresses, resolved DNS records, redirects, or ports. It does not block loopback, private, link-local, reserved, or cloud metadata address ranges. Requests originate from the Browserless environment rather than necessarily from the user's workstation. That environment may have access to internal dashboards, control planes, service APIs, or cloud metadata endpoints. Data extraction and JavaScript evaluation actions can return information obtained from those destinations. Arbitrary public website navigation is required for the declared functionality, but access to internal infrastructure and metadata endpoints is not required and violates least privilege. ### Attack Path 1. An attacker supplies an internal-service or metadata URL to a navigation-capable action. 2. Browserless resolves and requests the destination from its own network context. 3. The destination responds because it trusts the Browserless networ ...[truncated 884 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only explicitly required schemes, normally `http` and `https`. 2. Resolve hostnames before navigation and reject loopback, private, link-local, multicast, reserved, and unspecified addresses. 3. Explicitly block known cloud metadata destinations. 4. Re-resolve and validate every redirect target to prevent redirect-based bypasses. 5. Defend against DNS rebinding by validating the address used for the actual connection. 6. Consider an explicit destination-domain allowlist for sensitive deployments. 7. Apply equivalent validation to every action that accepts a URL, including `navigate`, `get_text`, `get_multiple`, `type_text`, `screenshot`, `pdf`, `fill_form`, `new_page`, and the legacy product action. 8. Enforce egress restrictions at the Browserless container or network layer so application-level bypasses cannot reach internal services. 9. Require user confirmation before visiting newly introduced or sensitive destinations. ]]>

T08 · Insecure Dependencies

Warning
Location
requirements.txt:1
Finding
Unpinned Playwright Dependency Creates Supply-Chain and Reproducibility Risk<![CDATA[ ## Vulnerability Details **File Location**: `requirements.txt:1` **Vulnerability Type**: Unbounded third-party dependency **Risk Level**: Medium ### Vulnerable Code ```text playwright ``` The documented installation command executes this unconstrained requirement: ```bash pip install -r requirements.txt ``` ### Technical Analysis The dependency specification does not pin Playwright to a reviewed version and does not provide integrity hashes. Each installation may therefore resolve to a different release based on the package index state at installation time. This creates non-reproducible builds and permits future compromised, malicious, incompatible, or behavior-changing releases to be installed without a source-code change in this project. The README mentions `playwright>=1.40.0`, but the effective requirements file does not enforce even that lower bound. No evidence was found that the current package name is typosquatted or intentionally malicious; the risk arises from unsafe dependency management. ### Attack Path 1. A user follows the documented installation process. 2. `pip` queries the configured package index for the latest package satisfying the unbounded `playwright` requirement. 3. A future compromised or malicious release is selected. 4. The package and its dependencies are installed with the privileges of the invoking user or build environment. 5. Malicious installation or runtime behavior can access credentials, source code, files, and network resources available to that environment. ### Impact Assessment A compromised dependency could execute code with the privileges of the installation or runtime account. Potential impact includes: - Credential and source-code theft - Modification of project files - Network exfiltration - Browser-session compromise - CI/CD environment compromise - Non-reproducible or unexpectedly broken deployments ]]>
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin Playwright to an exact reviewed version. 2. Generate a lock file containing hashes for Playwright and all transitive dependencies. 3. Install with hash verification, such as `pip install --require-hashes`. 4. Use an approved internal package mirror where appropriate. 5. Run dependency vulnerability and provenance scanning in CI. 6. Update pinned versions through a controlled review and testing process. 7. Keep the README dependency statement consistent with the effective lock file. 8. Perform installations in an isolated virtual environment or container using a non-privileged account. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (23)

Credential Access

High
Category
Privilege Escalation
Content
set BROWSERLESS_TOKEN=your-token-here
```

### Method 3: .env File

Create a `.env` file in the skill directory:
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
set BROWSERLESS_TOKEN=your-token-here
```

### Method 3: .env File

Create a `.env` file in the skill directory:
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description presents the skill as a web automation capability using a headless browser. However, the supplied code chunk only validates local configuration for a Browserless service by checking environment variables and WebSocket URL formatting. It prints diagnostics, recommendations, and example configuration, but does not launch a browser, connect to a website, navigate pages, scrape content, or automate interactions. This is a materially different primary purpose, so the description does not accurately represent the code shown.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This markdown file includes examples for taking screenshots, generating PDFs, and filling forms, and elsewhere advertises storage-modifying actions such as setting cookies and local storage. Under the markdown variant of SQP-2, the skill description should warn users when behavior can affect privacy, user data, or system integrity, but no such warning is provided near the usage guidance.

Rp1

Medium
Category
MCP Rug Pull
Confidence
75% confidence
Finding
Docker image references without a specific tag (:latest is implicit) or digest (@sha256:...) can be silently replaced by a malicious image.

Rp1

Medium
Category
MCP Rug Pull
Confidence
75% confidence
Finding
Docker image references without a specific tag (:latest is implicit) or digest (@sha256:...) can be silently replaced by a malicious image.

Rp1

Medium
Category
MCP Rug Pull
Confidence
75% confidence
Finding
Docker image references without a specific tag (:latest is implicit) or digest (@sha256:...) can be silently replaced by a malicious image.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This section documents sensitive browser capabilities such as screenshots, PDFs, form filling, storage/cookie handling, custom headers, and JavaScript evaluation, but does not place prominent privacy and data-impact warnings before encouraging use. In the context of a browser automation skill, these capabilities can capture credentials, session tokens, personal data, or internal content if used incautiously or auto-triggered by an agent.

Vague Triggers

Medium
Confidence
90% confidence
Finding
The README states the agent will automatically use this browser automation skill for broad natural-language requests, without clear consent, scope, or domain boundaries. In a skill that can navigate, fill forms, upload files, manipulate cookies, and run JavaScript, ambiguous auto-activation increases the risk of unintended actions on sensitive sites or data exposure triggered by loosely phrased prompts.

External Transmission

Medium
Category
Data Exfiltration
Content
```bash
# Generate PDF report
python main.py navigate '{"url": "https://analytics.com/report"}'
python main.py wait_for_selector '{"selector": ".chart-loaded"}'
python main.py pdf '{
  "path": "monthly-report.pdf",
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill exposes powerful capabilities including network access and use of environment-provided connection details, but the manifest declares no explicit tool scope or permission boundaries. In an agent ecosystem, missing scope declarations can cause overbroad invocation and reduce policy enforcement, making misuse of browser automation, data extraction, and remote interaction easier.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The interaction actions can type into forms, click buttons, upload files, and press keys on live websites, but the skill does not warn that these may trigger real submissions, purchases, account changes, or data disclosure. In an autonomous agent context, missing guardrails around state-changing web actions materially increases the risk of unintended external effects.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
Screenshot and PDF actions write captured page contents to files, but the documentation omits warnings that those files may contain personal data, credentials, internal documents, or regulated content. This raises the likelihood of sensitive information being stored insecurely, retained unnecessarily, or shared outside intended boundaries.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The skill explicitly supports reading and modifying cookies and localStorage but does not warn that these may contain session tokens, personal data, or other secrets. In agent-driven workflows, omission of this warning increases the risk of accidental credential extraction, session hijacking, or inappropriate retention of sensitive browser state.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The security notes assert that credentials are never logged or exposed, yet the documented actions allow reading cookies, localStorage values, and setting Authorization headers. This can mislead users and downstream agents into treating the skill as safer than it is, increasing the chance that secrets are extracted, surfaced in outputs, or mishandled.

Context-Inappropriate Capability

Medium
Confidence
92% confidence
Finding
The screenshot() and pdf() actions accept arbitrary filesystem paths and then write files locally without path restrictions. If an attacker controls arguments, they may overwrite application files, plant artifacts in sensitive locations, or abuse the agent host as a file-write primitive beyond normal browser automation intent.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The skill exposes arbitrary JavaScript execution through evaluate() and evaluate_function(), allowing callers to run attacker-controlled code in the browser context on any visited site. In an automation skill this goes beyond ordinary navigation/scraping and enables reading page state, extracting secrets from DOM/storage, triggering privileged in-session actions, and evading higher-level guardrails by moving logic into raw script execution.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
The skill can read, set, and delete cookies plus read and modify localStorage, which exposes browser session material and application data directly to the caller. Combined with open navigation and automation, this can be used to steal authenticated session data, impersonate users, tamper with application state, or bypass intended authentication flows.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
get_cookies() returns all cookies or a named cookie with no warning, consent, or filtering, which can directly expose session identifiers and other authentication tokens. In a browser automation context this materially increases account-takeover risk because the skill can visit authenticated sites and then exfiltrate the resulting session data.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
get_local_storage() reads arbitrary localStorage keys directly from the active origin, which may contain tokens, profile data, CSRF material, or application secrets. Because the skill can navigate anywhere first, this becomes a generic sensitive-data extraction primitive without any user awareness or access controls.

Intent-Code Divergence

Low
Confidence
89% confidence
Finding
The changelog asserts that credentials are never logged, yet the documented setup and support text instruct users to use token-bearing connection strings such as BROWSERLESS_WS with a query-parameter token. Secrets passed in shell commands or stored in shell startup/history can be exposed through terminal history, CI logs, screenshots, or process/environment inspection, making the claim misleading and increasing accidental credential disclosure risk.

Intent-Code Divergence

Low
Confidence
98% confidence
Finding
The manifest metadata and setup instructions consistently require `BROWSERLESS_URL` (L006, L030-L037), but the troubleshooting section tells users to verify `BROWSERLESS_WS`. This contradicts the documented configuration contract and can mislead users about which environment variable the skill actually uses.

Unpinned Dependencies

Low
Category
Supply Chain
Content
playwright
Confidence
95% confidence
Finding
The dependency is specified without a version pin, so installs may pull different Playwright releases over time, including breaking changes or a compromised upstream version. In a browser automation skill, Playwright is a powerful dependency with network, browser, and filesystem interaction capabilities, so uncontrolled version drift increases supply-chain risk beyond a purely cosmetic packaging issue.

Static analysis

No suspicious patterns detected.