SkillTotal

Is browser-use/browser-use safe?

browser-use is an AI python_package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 10 risky constructs are reported for review. It can: filesystem read, filesystem write, mcp tools detected, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 80/100 (critical).

browser-use 0.13.3

python_package · https://github.com/browser-use/browser-use
CRITICAL
80
/ 100 risk score
Snapshot · scanned Jul 2, 2026 · browser-use@0.13.3 · engine 0.24.0 / ruleset 25
High-risk capabilities - review before installing
Notable — review in context (capabilities are not malware):
  • Sensitive-data access combined with network egress
  • Python shell/command execution
  • Remote pipe-to-shell execution

No malicious indicators found by static analysis.

Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of browser-use/browser-use's authors. Report a false positive.

Capabilities — what this component can do (not a risk score):
filesystem readfilesystem writemcp tools detectednetwork egressshell execution

Findings (10)

CRITICALSensitive-data access combined with network egressST-COMBO-EXFIL

The component both reads secret locations (keys, tokens) and can send data over the network.

PROJECT_API_KEY = 'phc_…[redacted, 47 chars]'
from urllib.parse import urlparse
cdp_url=urlparse(self.browser_session.cdp_url).hostname
from urllib.parse import urlparse

Why it matters: That combination is the classic path for quietly stealing credentials off your machine.

Fix: Verify that secrets read from disk are never transmitted off-host without explicit, auditable user consent.

HIGHDangerous MCP tool capabilityST-MCP-DANGEROUS-TOOL

An MCP tool exposes a powerful capability (files, shell, network, browser, or credentials).

"name": "retry_with_browser_use_agent",
"name": "browser_navigate",
"name": "browser_get_state",
"name": "browser_extract_content",
"name": "browser_go_back",
"name": "browser_list_tabs",
"name": "browser_switch_tab",
"name": "browser_close_tab",

Why it matters: Wired into an agent, these grant it real access to your machine — confirm each is required.

Fix: Confirm each powerful tool is required and constrained; broad MCP tools (shell/filesystem/network) grant an agent significant host access.

HIGHEmbedded secret / credentialST-SECRET-EMBEDDED

A hardcoded credential (API key, token, or private key) is shipped in the code.

PROJECT_API_KEY = 'phc_…[redacted, 47 chars]'

Why it matters: Anyone who gets the package gets the secret — rotate it and load secrets at runtime instead.

Fix: Remove the secret from the code, rotate it immediately, and load credentials from the environment or a secrets manager at runtime.

HIGHRemote pipe-to-shell executionST-SHELL-PIPE-EXEC

A remotely fetched payload is piped straight into a shell (e.g. `curl … | bash`). The component runs code downloaded at runtime, which is unreviewable and a common second-stage delivery vector.

curl -LsSf https://astral.sh/uv/install.sh | sh

Fix: Download to a file, inspect it, and run a pinned/verified copy instead of piping a network response directly into a shell.

HIGHPython shell/command executionST-SHELL-PY

The component can run operating-system commands or spawn processes.

result = subprocess.run(
			[str(binary), '--help'],
			capture_output=True,
			text=True,
			timeout=5,
			check=False,
		)
self.process = await asyncio.create_subprocess_exec(
				*self.command,
				stdin=asyncio.subprocess.PIPE,
				stdout=asyncio.subprocess.PIPE,
				stderr=asyncio.subprocess.PIPE,
				env=self.env,
				limit=self.stream_limit,
			)
result = subprocess.run(['which', cmd], capture_output=True, text=True)
subprocess = await asyncio.create_subprocess_exec(
					browser_path,
					*launch_args,
					stdout=asyncio.subprocess.PIPE,
					stderr=asyncio.subprocess.PIPE,
				)
process = await asyncio.create_subprocess_exec(
			*cmd,
			stdout=asyncio.subprocess.PIPE,
			stderr=asyncio.subprocess.PIPE,
		)
result = subprocess.run(cmd)
result = subprocess.run([uv, 'tool', 'install', '--python', '3.12', '--upgrade', '--force', 'browser-use'])
result = subprocess.run([exe, 'skill'], capture_output=True, text=True)
subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=package_root, stderr=subprocess.DEVNULL).decode().strip()
subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=package_root, stderr=subprocess.DEVNULL)
subprocess.check_output(['git', 'config', '--get', 'remote.origin.url'], cwd=package_root, stderr=subprocess.DEVNULL)
subprocess.check_output(['git', 'show', '-s', '--format=%ci', 'HEAD'], cwd=package_root, stderr=subprocess.DEVNULL)
subprocess.run(['open', file_path], check=True)
subprocess.run(['cmd', '/c', 'start', '', file_path], check=True)
subprocess.run(['xdg-open', file_path], check=True)
test_proc = await asyncio.create_subprocess_exec(
					path, '--version', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
				)
process = await asyncio.create_subprocess_exec(*cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

Why it matters: Powerful and often legitimate — confirm the commands aren't built from untrusted input.

Fix: Confirm the command and its arguments are fully controlled and not derived from untrusted input; avoid shell=True.

MEDIUMServer bound to all network interfacesST-EXPOSE-BIND

A server is bound to all network interfaces (0.0.0.0), not just your own machine.

uvicorn.run('integrations.slack.slack_api:app', host='0.0.0.0', port=3000)

Why it matters: Without authentication, other hosts on the network can reach it.

Fix: Bind to 127.0.0.1 for local-only use, or require authentication and restrict access if remote exposure is intended.

MEDIUMPython filesystem readST-FS-PY-READ

The component reads files from disk.

with open(path, 'rb') as f:
importlib.resources.files('browser_use.agent.system_prompts')
				.joinpath(template_filename)
				.open('r', encoding='utf-8') as f
with open(filepath, encoding='utf-8') as f:
loaded = json.loads(path.read_text())
data = base64.b64encode(image_path.read_bytes()).decode('ascii')
with open(file_path, encoding='utf-8') as history_file:
with open(local_state_path, encoding='utf-8') as f:
with open(manifest_path, encoding='utf-8') as f:
with open(bg_path, encoding='utf-8') as f:
with open(crx_path, 'rb') as f:
with open(path_obj, 'rb') as f:
existing_state = json.loads(json_path.read_text())
content = await anyio.Path(str(load_path)).read_text()
if Path('/.dockerenv').exists() or 'docker' in Path('/proc/1/cgroup').read_text().lower():
with open(config_path) as f:
with open(module.__file__) as f:
return skill_path.read_text(encoding='utf-8')
text = resources.files('browser_harness').joinpath('SKILL.md').read_text(encoding='utf-8')
device_id = device_id_path.read_text().strip()
with open(config_path) as f:
with open(self.USER_ID_PATH) as f:
cached = CachedPricingData.model_validate_json(await anyio.Path(cache_file).read_text())
content = await anyio.Path(cache_file).read_text()
cached = CachedPricingData.model_validate_json(cache_file.read_text())
with open(pyproject_path, encoding='utf-8') as f:

Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.

Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.

MEDIUMPython filesystem write/deleteST-FS-PY-WRITE

The component writes or deletes files on disk.

await anyio.Path(target_path).write_text(
		await _format_conversation(input_messages, response),
		encoding=encoding or 'utf-8',
	)
with open(filepath, 'w', encoding='utf-8') as f:
target.write_text(
			json.dumps(self._conversation_snapshot(), indent=2, default=str),
			encoding=self.settings.save_conversation_path_encoding or 'utf-8',
		)
shutil.copytree(
					path_original_profile,
					path_temp_profile,
					ignore=_ignore_chrome_profile_transient_files,
				)
shutil.rmtree(temp_dir, ignore_errors=True)
shutil.copy(local_state_src, local_state_dst)
with open(bg_path, 'w', encoding='utf-8') as f:
with open(output_path, 'wb') as f:
output_file.write_text(json.dumps(storage_state, indent=2, ensure_ascii=False), encoding='utf-8')
Path(path).write_bytes(screenshot_data)
(sidecar_dir / filename).write_bytes(body_bytes)
(sidecar_dir / req_filename).write_bytes(post_data_bytes)
tmp_path.write_bytes(json.dumps(har_obj, indent=2, ensure_ascii=False).encode('utf-8'))
temp_path.write_text(json.dumps(merged_state, indent=4, ensure_ascii=False), encoding='utf-8')
with open(config_path, 'w') as f:
with open(config_path, 'w') as f:
with open(config_path, 'w') as f:
file_path.write_text(self.content)
await asyncio.get_event_loop().run_in_executor(executor, lambda: file_path.write_text(self.content))

Why it matters: Usually legitimate, but worth confirming the paths can't be controlled by untrusted input.

Fix: Confirm which files are written/deleted and that paths cannot be influenced by untrusted input.

MEDIUMPython network egressST-NET-PY

The component makes outbound network requests.

from urllib.parse import urlparse
cdp_url=urlparse(self.browser_session.cdp_url).hostname
from urllib.parse import urlparse
cdp_url=urlparse(cdp_url).hostname if cdp_url else None,
current_parts = urlparse(current)
requested_parts = urlparse(requested)
self.client = httpx.AsyncClient(timeout=30.0)
from urllib.parse import urlparse
with urllib.request.urlopen(url) as response:
from urllib.parse import urlparse, urlunparse
host = urlparse(self.cdp_url).hostname or ''
parsed_url = urlparse(self.cdp_url)
url = urlunparse(
				(parsed_url.scheme, parsed_url.netloc, path, parsed_url.params, parsed_url.query, parsed_url.fragment)
			)
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0), trust_env=not is_localhost) as client:
from urllib.parse import urlparse
filename = urlparse(url).path.split('/')[-1]
return Path(urlparse(value).path if '://' in value else value).suffix.lower().lstrip('.') in _NETWORK_DOWNLOAD_FILE_EXTENSIONS

Why it matters: Usually legitimate, but confirm the destinations are expected and no sensitive data leaves.

Fix: Confirm the destination hosts are expected and that no sensitive data is sent off-host.

LOWMCP tool surface detectedST-MCP-DETECTED

An MCP tool surface (manifest or tool definitions) was found.

Why it matters: Just context — review which tools it offers and their permissions.

Fix: Review the declared MCP tools and their permissions.

Check your own component

Run the same evidence-backed scan on any MCP server, agent skill, or package.

Scan your own component

Or get notified if this component's risk changes:

How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →