Back to skill

Security audit

AIGroup Browser Skill

Security checks for vulnerabilities and agentic risk

Overview

The skill is a real-browser opener as advertised, but it gives agents broad access to persistent browser profiles and can expose unrelated tab metadata, so it needs review before installation.

Install only in an environment where agents are trusted to open arbitrary URLs in existing browser profiles. Before broader use, add URL validation, block local/private/internal and non-http(s) destinations unless explicitly needed, remove the unrelated-tab fallback, and prefer isolated or temporary browser profiles for untrusted tasks.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/open_page.py:95
Finding

Unrestricted URL Navigation Through Privileged Browser Profiles

Content
View full analysis
None: subprocess.run([cmd, url], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) ``` ```python url = req.get("url") if not url or not isinstance(url, str): print("Error: url is required", file=sys.stderr) return 1 mode = str(req.get("mode", "auto")).lower() timeout = int(req.get("timeout", 20)) cmd, chosen_mode = choose(mode) port = resolve_port(mode, url) open_url(cmd, url) ``` ### Technical Analysis The input validation only verifies that `url` is a non-empty string. It does not enforce an `http` or `https` scheme, reject local browser schemes, validate the hostname, or restrict access to loopback, link-local, and private-network destinations. The unvalidated value is passed as an argument to one of the fixed external browser wrappers: - `/home/jack/.local/bin/oc-cn` - `/home/jack/.local/bin/oc-global` - `/home/jack/.local/bin/oc-browser` The use of an argument list rather than `shell=True` prevents conventional shell metacharacter injection. However, it does not prevent the browser or wrapper from interpreting the supplied value as an unsupported scheme, an internal-network destination, or potentially a command-line option. The behavior of the external wrappers could not be verified because their implementations are not included in the audited project. The risk is increased because the Skill intentionally uses persistent “real browser” profiles, which may contain authenticated sessions unavailable to an ordinary unauthenticated HTTP client. ### Attack Path 1. An attacker or untrusted caller supplies a crafted request containing a loopback, private-network, link-local, or non-web URL. 2. `read_request()` parses the input without applying URL security policy. ...[truncated 1010 chars]
Remediation
View remediation
str: parsed = urllib.parse.urlsplit(value) if parsed.scheme.lower() not in {"https", "http"}: raise ValueError("only HTTP and HTTPS URLs are allowed") if not parsed.hostname: raise ValueError("URL must contain a hostname") if parsed.username is not None or parsed.password is not None: raise ValueError("embedded credentials are not allowed") for result in socket.getaddrinfo(parsed.hostname, parsed.port): address = ipaddress.ip_address(result[4][0]) if ( address.is_private or address.is_loopback or address.is_link_local or address.is_multicast or address.is_reserved or address.is_unspecified ): raise ValueError("internal or reserved destinations are not allowed") return value ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/open_page.py:55
Finding

Disclosure of Unrelated Browser Tab Metadata Through Unsafe Fallback Selection

Content
View full analysis
dict[str, Any] | None: matching = [p for p in pages if same_target(p.get("url", ""), requested_url)] if not matching: return pages[0] if pages else None titled = [p for p in matching if (p.get("title") or "").strip()] return titled[0] if titled else matching[0] ``` ```python candidate = pick_candidate(pages, requested_url) if candidate: fallback = candidate if (candidate.get("title") or "").strip(): return candidate ``` ```python out = { "ok": True, "mode": effective_mode, "port": port, "title": (page.get("title") or "").strip(), "url": page.get("url", ""), } ``` ### Technical Analysis When no CDP page has the same hostname as the requested URL, `pick_candidate()` returns `pages[0]`. That page may be an unrelated tab that was already open in the shared CN or global browser profile. If this unrelated page has a non-empty title, `wait_for_page()` immediately accepts it. The caller then receives its title and URL in a response marked `"ok": true`, even though the page is not associated with the requested destination. The hostname comparison does not protect this fallback path. Once the `matching` list is empty, the code explicitly discards target correlation and selects the first available page. This creates both an information-disclosure issue and an integrity issue: unrelated browsing metadata is exposed, and the Skill incorrectly reports a successful browser operation. ### Attack Path 1. A browser profile already contains one or more tabs, potentially including authenticated or sensitive pages. 2. An attacker submits a URL whose resulting browser target is not observed on ...[truncated 1090 chars]
Remediation
View remediation
dict[str, Any] | None: matching = [ page for page in pages if same_target(page.get("url", ""), requested_url) ] if not matching: return None titled = [ page for page in matching if (page.get("title") or "").strip() ] return titled[0] if titled else matching[0] ``` For stronger isolation, snapshot target IDs before navigation and only inspect a newly created target: ```python before_ids = {page.get("id") for page in list_pages(port)} open_url(cmd, url) # During polling, consider only targets not present in before_ids. new_pages = [ page for page in list_pages(port) if page.get("id") not in before_ids ] ``` ]]>
Vulnerability Patterns
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (2)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill instructs the agent to invoke local binaries and open arbitrary URLs in a real browser, which gives it effective shell and network capabilities, but it declares no explicit tool scope or permission boundaries. That mismatch is dangerous because downstream systems or reviewers cannot reliably enforce least privilege, and the skill could be used to access sensitive internal resources or launch unintended browser-driven actions.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/open_page.py (reported line 50)May include surrounding context.

python
def open_url(cmd: str, url: str) -> None:
    subprocess.run([cmd, url], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)


def same_target(page_url: str, requested_url: str) -> bool:

Static analysis

No suspicious patterns detected.