diff --git a/README.md b/README.md index 356a08c..990834a 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,11 @@ All tests verified against live detection services. Last tested: Feb 2026 (Chrom ### Proof +

+Cloudflare Turnstile — 3 Tests Passing (Headed Mode) +
Cloudflare Turnstile — 3 live tests passing in headed mode (macOS) +

+

reCAPTCHA v3 — Score 0.9
reCAPTCHA v3 score 0.9 — server-side verified (human-level) diff --git a/cloakbrowser/download.py b/cloakbrowser/download.py index 2eb0fcf..3137448 100644 --- a/cloakbrowser/download.py +++ b/cloakbrowser/download.py @@ -8,7 +8,9 @@ from __future__ import annotations import logging import os +import platform import stat +import subprocess import tarfile import tempfile import threading @@ -168,27 +170,39 @@ def _extract_archive( dest_dir.mkdir(parents=True, exist_ok=True) with tarfile.open(archive_path, "r:gz") as tar: - # Security: prevent path traversal and symlink attacks + # Security: prevent path traversal safe_members = [] for member in tar.getmembers(): + # Allow symlinks — macOS .app bundles require them (Framework layout) if member.issym() or member.islnk(): - logger.warning("Skipping symlink in archive: %s", member.name) - continue - member_path = (dest_dir / member.name).resolve() - if not str(member_path).startswith(str(dest_dir.resolve())): - raise RuntimeError(f"Archive contains path traversal: {member.name}") + link_target = member.linkname + # Reject symlinks that escape the dest dir + if os.path.isabs(link_target) or ".." in link_target.split("/"): + logger.warning("Skipping suspicious symlink: %s -> %s", member.name, link_target) + continue + else: + member_path = (dest_dir / member.name).resolve() + if not str(member_path).startswith(str(dest_dir.resolve())): + raise RuntimeError(f"Archive contains path traversal: {member.name}") safe_members.append(member) tar.extractall(dest_dir, members=safe_members) # If tar extracted into a single subdirectory, flatten it # (e.g. fingerprint-chromium-142-custom-v2/chrome → chrome) + # But never flatten .app bundles — macOS needs the bundle structure intact _flatten_single_subdir(dest_dir) # Make binary executable bp = binary_path or get_binary_path() if bp.exists(): _make_executable(bp) + + # macOS: remove quarantine/provenance xattrs to prevent Gatekeeper prompts + if platform.system() == "Darwin": + _remove_quarantine(dest_dir) + + if bp.exists(): logger.info("Binary ready: %s", bp) @@ -203,6 +217,10 @@ def _flatten_single_subdir(dest_dir: Path) -> None: entries = list(dest_dir.iterdir()) if len(entries) == 1 and entries[0].is_dir(): subdir = entries[0] + # Never flatten .app bundles — macOS needs the bundle structure + if subdir.name.endswith(".app"): + logger.debug("Keeping .app bundle intact: %s", subdir.name) + return logger.debug("Flattening single subdirectory: %s", subdir.name) for item in subdir.iterdir(): shutil.move(str(item), str(dest_dir / item.name)) @@ -220,6 +238,19 @@ def _make_executable(path: Path) -> None: path.chmod(current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) +def _remove_quarantine(path: Path) -> None: + """Remove macOS quarantine/provenance xattrs so Gatekeeper doesn't block the binary.""" + try: + subprocess.run( + ["xattr", "-cr", str(path)], + capture_output=True, + timeout=30, + ) + logger.debug("Removed quarantine attributes from %s", path) + except Exception: + logger.debug("Failed to remove quarantine attributes", exc_info=True) + + def clear_cache() -> None: """Remove all cached binaries. Forces re-download on next launch.""" from .config import get_cache_dir diff --git a/js/src/download.ts b/js/src/download.ts index ed9bbdb..0c7c388 100644 --- a/js/src/download.ts +++ b/js/src/download.ts @@ -4,6 +4,7 @@ * Mirrors Python cloakbrowser/download.py. */ +import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { pipeline } from "node:stream/promises"; @@ -266,6 +267,14 @@ async function extractArchive( const bp = binaryPath || getBinaryPath(); if (fs.existsSync(bp)) { fs.chmodSync(bp, 0o755); + } + + // macOS: remove quarantine/provenance xattrs to prevent Gatekeeper prompts + if (process.platform === "darwin") { + removeQuarantine(destDir); + } + + if (fs.existsSync(bp)) { console.log(`[cloakbrowser] Binary ready: ${bp}`); } } @@ -278,6 +287,8 @@ function flattenSingleSubdir(destDir: string): void { const entries = fs.readdirSync(destDir); if (entries.length === 1) { const subdir = path.join(destDir, entries[0]!); + // Never flatten .app bundles — macOS needs the bundle structure + if (entries[0]!.endsWith(".app")) return; if (fs.statSync(subdir).isDirectory()) { const children = fs.readdirSync(subdir); for (const child of children) { @@ -291,6 +302,15 @@ function flattenSingleSubdir(destDir: string): void { } } +/** Remove macOS quarantine/provenance xattrs so Gatekeeper doesn't block the binary. */ +function removeQuarantine(dirPath: string): void { + try { + execFileSync("xattr", ["-cr", dirPath], { timeout: 30_000 }); + } catch { + // Non-fatal — user can manually run: xattr -cr ~/.cloakbrowser/ + } +} + function isExecutable(filePath: string): boolean { try { fs.accessSync(filePath, fs.constants.X_OK);