5cec57e06d
Testing / remote-protocol-compat (0.9.3) (push) Successful in 40s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 38s
Testing / test (push) Failing after 1m3s
Package Extension / package-extension (push) Successful in 29s
Build & Publish Package / publish (push) Successful in 33s
- Add safe-by-default policy gates for raw command surfaces: command, script, and serve-http /command. - Require explicit opt-ins for page reads, browser control, and high-risk commands such as dom.eval, storage.*, and screenshots. - Remove all cookies support from CLI, SDK, extension commands, permissions, constants, docs, and tests. - Add diagnostic, events, watch, workspace, remote, raw command, script, HTTP gateway, tree-view, session import/export, and extension info/capability commands. - Add Chrome Web Store packaging that strips manifest.key while keeping local packages with a stable native-messaging extension ID. - Bump browser-cli and extension version to 0.14.1 and cover the new behavior with pytest and extension packaging tests. BREAKING CHANGE: cookies commands and the b.cookies SDK namespace have been removed; generic raw command execution now blocks non-safe commands unless explicitly allowed.
120 lines
2.7 KiB
Python
120 lines
2.7 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 = {
|
|
"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.",
|
|
)
|
|
|
|
@dataclass(frozen=True)
|
|
class CommandPolicy:
|
|
allow_read_page: bool = False
|
|
allow_control: bool = False
|
|
allow_dangerous: bool = False
|
|
|
|
@classmethod
|
|
def unrestricted(cls) -> "CommandPolicy":
|
|
return cls(allow_read_page=True, allow_control=True, allow_dangerous=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 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
|
|
raise PermissionError(
|
|
f"Raw command '{command}' is {category} and blocked by default; "
|
|
"use --allow-read-page, --allow-control, or --allow-dangerous explicitly"
|
|
)
|