fix: harden IPC, screenshot, paging, and tab filter error handling
- tabs.py: validate screenshot data URL prefix and catch binascii.Error instead of silently writing a zero-byte file or crashing with a raw traceback - serve.py: add 30 s recv timeout on client connections to prevent unbounded thread accumulation; use hmac.compare_digest for constant-time token check - native_host.py: bind Unix socket before _registry_add to eliminate the window where the registry points to an unbound path; cap paging loop at ceil(10000/PAGE_SIZE) iterations to guard against a misbehaving extension; remove dead no-hello fast-path queue that was registered but never consumed - __init__.py: narrow _apply_tab_filter except to (AttributeError, TypeError) so broken filter functions raise instead of silently returning wrong results Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -685,8 +685,8 @@ class BrowserCLI:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
transformed = filter_fn(tabs)
|
transformed = filter_fn(tabs)
|
||||||
except Exception:
|
except (AttributeError, TypeError):
|
||||||
transformed = None
|
return [tab for tab in tabs if filter_fn(tab)]
|
||||||
|
|
||||||
if isinstance(transformed, list):
|
if isinstance(transformed, list):
|
||||||
return transformed
|
return transformed
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import threading, secrets, socket, struct, click, json, sys
|
import hmac, threading, secrets, socket, struct, click, json, sys
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv
|
|||||||
command = msg.get("command", "?")
|
command = msg.get("command", "?")
|
||||||
|
|
||||||
if server_token is not None:
|
if server_token is not None:
|
||||||
if msg.get("token") != server_token:
|
if not hmac.compare_digest(msg.get("token") or "", server_token):
|
||||||
_send_error(msg_id, "unauthorized: invalid or missing token")
|
_send_error(msg_id, "unauthorized: invalid or missing token")
|
||||||
_log(addr, command, None, "DENIED", "bad token")
|
_log(addr, command, None, "DENIED", "bad token")
|
||||||
return
|
return
|
||||||
@@ -110,6 +110,7 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv
|
|||||||
_log(addr, command, resolved_profile, "ERROR", str(e))
|
_log(addr, command, resolved_profile, "ERROR", str(e))
|
||||||
|
|
||||||
def _handle_client(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None) -> None:
|
def _handle_client(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None) -> None:
|
||||||
|
client_sock.settimeout(30)
|
||||||
with client_sock:
|
with client_sock:
|
||||||
_proxy_request(client_sock, addr, profile, server_token)
|
_proxy_request(client_sock, addr, profile, server_token)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import binascii
|
||||||
import click
|
import click
|
||||||
from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command
|
from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
@@ -288,7 +289,12 @@ def tabs_screenshot(output, tab_id, fmt, quality):
|
|||||||
data_url = result.get("dataUrl", "") if isinstance(result, dict) else ""
|
data_url = result.get("dataUrl", "") if isinstance(result, dict) else ""
|
||||||
if output:
|
if output:
|
||||||
header = f"data:image/{fmt};base64,"
|
header = f"data:image/{fmt};base64,"
|
||||||
raw = base64.b64decode(data_url[len(header):])
|
if not data_url.startswith(header):
|
||||||
|
raise click.ClickException("Empty or unexpected screenshot response (incognito/protected tab?)")
|
||||||
|
try:
|
||||||
|
raw = base64.b64decode(data_url[len(header):])
|
||||||
|
except binascii.Error as e:
|
||||||
|
raise click.ClickException(f"Failed to decode screenshot data: {e}")
|
||||||
with open(output, "wb") as f:
|
with open(output, "wb") as f:
|
||||||
f.write(raw)
|
f.write(raw)
|
||||||
console.print(f"[green]Screenshot saved:[/green] {output}")
|
console.print(f"[green]Screenshot saved:[/green] {output}")
|
||||||
|
|||||||
+28
-17
@@ -7,6 +7,7 @@ It relays messages between extension (stdin/stdout Native Messaging protocol)
|
|||||||
and CLI (local IPC endpoint: Unix socket on Unix, named pipe on Windows).
|
and CLI (local IPC endpoint: Unix socket on Unix, named pipe on Windows).
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import socket
|
import socket
|
||||||
@@ -121,7 +122,7 @@ def stdin_reader(alias: str):
|
|||||||
|
|
||||||
# --- Thread B: accept CLI socket connections ---
|
# --- Thread B: accept CLI socket connections ---
|
||||||
|
|
||||||
def socket_server(sock_path: str):
|
def socket_server(sock_path: str, bound_sock: "socket.socket | None" = None):
|
||||||
if is_windows():
|
if is_windows():
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -132,13 +133,14 @@ def socket_server(sock_path: str):
|
|||||||
threading.Thread(target=handle_cli_connection, args=(conn, listener), daemon=True).start()
|
threading.Thread(target=handle_cli_connection, args=(conn, listener), daemon=True).start()
|
||||||
return
|
return
|
||||||
|
|
||||||
path = Path(sock_path)
|
sock = bound_sock
|
||||||
if path.exists():
|
if sock is None:
|
||||||
path.unlink()
|
path = Path(sock_path)
|
||||||
|
if path.exists():
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
path.unlink()
|
||||||
sock.bind(sock_path)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
sock.listen(16)
|
sock.bind(sock_path)
|
||||||
|
sock.listen(16)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -212,8 +214,13 @@ def _collect_paged_browser_command(cmd: dict) -> dict:
|
|||||||
offset = 0
|
offset = 0
|
||||||
items = []
|
items = []
|
||||||
total = None
|
total = None
|
||||||
|
max_pages = math.ceil(10_000 / PAGE_SIZE)
|
||||||
|
pages_fetched = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
if pages_fetched >= max_pages:
|
||||||
|
return {"id": original_id, "success": False, "error": f"paging loop exceeded {max_pages} pages — extension bug?"}
|
||||||
|
pages_fetched += 1
|
||||||
page_cmd = dict(cmd)
|
page_cmd = dict(cmd)
|
||||||
page_cmd["id"] = str(uuid.uuid4())
|
page_cmd["id"] = str(uuid.uuid4())
|
||||||
page_args = dict(cmd.get("args") or {})
|
page_args = dict(cmd.get("args") or {})
|
||||||
@@ -284,21 +291,25 @@ def main():
|
|||||||
if first_msg and first_msg.get("type") == "hello":
|
if first_msg and first_msg.get("type") == "hello":
|
||||||
alias = _resolve_profile_alias(first_msg)
|
alias = _resolve_profile_alias(first_msg)
|
||||||
else:
|
else:
|
||||||
# No hello — use a generated alias and re-queue the first command if needed.
|
# No hello — use a generated alias; first_msg is dropped (no response path).
|
||||||
alias = str(uuid.uuid4())
|
alias = str(uuid.uuid4())
|
||||||
if first_msg:
|
|
||||||
msg_id = first_msg.get("id")
|
|
||||||
if msg_id:
|
|
||||||
q: queue.Queue = queue.Queue()
|
|
||||||
with PENDING_LOCK:
|
|
||||||
PENDING[msg_id] = q
|
|
||||||
write_native_message(sys.stdout.buffer, first_msg)
|
|
||||||
|
|
||||||
runtime_dir().mkdir(mode=0o700, exist_ok=True)
|
runtime_dir().mkdir(mode=0o700, exist_ok=True)
|
||||||
sock_path = _socket_path_for(alias)
|
sock_path = _socket_path_for(alias)
|
||||||
|
|
||||||
|
if not is_windows():
|
||||||
|
path = Path(sock_path)
|
||||||
|
if path.exists():
|
||||||
|
path.unlink()
|
||||||
|
bound_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
bound_sock.bind(sock_path)
|
||||||
|
bound_sock.listen(16)
|
||||||
|
else:
|
||||||
|
bound_sock = None
|
||||||
|
|
||||||
_registry_add(alias, sock_path)
|
_registry_add(alias, sock_path)
|
||||||
|
|
||||||
t = threading.Thread(target=socket_server, args=(sock_path,), daemon=True)
|
t = threading.Thread(target=socket_server, args=(sock_path,), kwargs={"bound_sock": bound_sock}, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
stdin_reader(alias)
|
stdin_reader(alias)
|
||||||
|
|||||||
Reference in New Issue
Block a user