6fa931aa36
Testing / remote-protocol-compat (0.9.5) (push) Successful in 56s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 59s
Testing / test (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 33s
Package Extension / package-extension (push) Successful in 36s
- Gate TCP serve commands with safe-by-default policies, per-key allow tokens, per-key rate limiting, and audit labels. - Reuse authenticated encrypted remote sessions and parallelize/caches multi-browser fanout to reduce repeated handshake roundtrips. - Increase paged native-host batch size with extension-side byte budgeting to speed large tab listings safely. - Point install output at public Chrome Web Store / Firefox AMO listings by default, with --dev preserving unpacked workflows. - Share search-engine metadata between CLI and SDK and bump the package/extension version to 0.16.0. - Cover the new security, pooling, paging, install, and fanout behavior with expanded Python and extension tests.
133 lines
3.1 KiB
Python
133 lines
3.1 KiB
Python
"""Safety policy for generic command execution surfaces.
|
|
|
|
Dedicated first-party CLI/SDK methods keep their normal behavior. This module
|
|
only gates raw surfaces where a single string can trigger arbitrary browser
|
|
capabilities: ``browser-cli command``, ``browser-cli script``, and the HTTP
|
|
``/command`` endpoint.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
SAFE_COMMANDS = {
|
|
"browser-cli.targets",
|
|
"clients.list",
|
|
"extension.capabilities",
|
|
"extension.info",
|
|
"group.list",
|
|
"group.query",
|
|
"group.tabs",
|
|
"page.info",
|
|
"perf.status",
|
|
"tabs.active_in_window",
|
|
"tabs.count",
|
|
"tabs.filter",
|
|
"tabs.list",
|
|
"tabs.query",
|
|
"tabs.status",
|
|
"windows.list",
|
|
}
|
|
|
|
READ_PAGE_COMMANDS = {
|
|
"dom.attr",
|
|
"dom.exists",
|
|
"dom.query",
|
|
"dom.text",
|
|
"extract.html",
|
|
"extract.images",
|
|
"extract.json",
|
|
"extract.links",
|
|
"extract.markdown",
|
|
"extract.text",
|
|
"tabs.html",
|
|
}
|
|
|
|
CONTROL_PREFIXES = (
|
|
"navigate.",
|
|
"nav.",
|
|
"group.",
|
|
"session.",
|
|
"tabs.",
|
|
"windows.",
|
|
)
|
|
CONTROL_COMMANDS = {
|
|
"dom.check",
|
|
"dom.clear",
|
|
"dom.click",
|
|
"dom.focus",
|
|
"dom.hover",
|
|
"dom.key",
|
|
"dom.poll",
|
|
"dom.scroll",
|
|
"dom.select",
|
|
"dom.submit",
|
|
"dom.type",
|
|
"dom.uncheck",
|
|
"dom.wait_for",
|
|
"extension.reload",
|
|
}
|
|
|
|
DANGEROUS_COMMANDS = {
|
|
"dom.eval",
|
|
"tabs.screenshot",
|
|
}
|
|
DANGEROUS_PREFIXES = (
|
|
"storage.",
|
|
)
|
|
|
|
# Server-side key-management control commands. Gated separately so a key can be
|
|
# trusted for browser use without also being able to list or add trusted keys.
|
|
KEY_COMMANDS = {
|
|
"browser-cli.auth.keys",
|
|
"browser-cli.auth.trust",
|
|
}
|
|
|
|
@dataclass(frozen=True)
|
|
class CommandPolicy:
|
|
allow_read_page: bool = False
|
|
allow_control: bool = False
|
|
allow_dangerous: bool = False
|
|
allow_keys: bool = False
|
|
|
|
@classmethod
|
|
def unrestricted(cls) -> "CommandPolicy":
|
|
return cls(allow_read_page=True, allow_control=True, allow_dangerous=True, allow_keys=True)
|
|
|
|
def _is_control(command: str) -> bool:
|
|
if command in CONTROL_COMMANDS:
|
|
return True
|
|
if any(command.startswith(prefix) for prefix in CONTROL_PREFIXES):
|
|
return command not in SAFE_COMMANDS and command not in READ_PAGE_COMMANDS and command not in DANGEROUS_COMMANDS
|
|
return False
|
|
|
|
def command_category(command: str) -> str:
|
|
name = str(command or "")
|
|
if name in KEY_COMMANDS:
|
|
return "keys"
|
|
if name in DANGEROUS_COMMANDS or any(name.startswith(prefix) for prefix in DANGEROUS_PREFIXES):
|
|
return "dangerous"
|
|
if name in READ_PAGE_COMMANDS:
|
|
return "read-page"
|
|
if name in SAFE_COMMANDS:
|
|
return "safe"
|
|
if _is_control(name):
|
|
return "control"
|
|
return "unknown"
|
|
|
|
def assert_command_allowed(command: str, policy: CommandPolicy) -> None:
|
|
category = command_category(command)
|
|
if category == "safe":
|
|
return
|
|
if category == "read-page" and policy.allow_read_page:
|
|
return
|
|
if category == "control" and policy.allow_control:
|
|
return
|
|
if category == "dangerous" and policy.allow_dangerous:
|
|
return
|
|
if category == "keys" and policy.allow_keys:
|
|
return
|
|
raise PermissionError(
|
|
f"Raw command '{command}' is {category} and blocked by default; "
|
|
"use --allow-read-page, --allow-control, --allow-dangerous, or --allow-keys explicitly"
|
|
)
|