From b79f7d65b6d1480fe9dbe146f2fd0fe793607470 Mon Sep 17 00:00:00 2001 From: CodePilot Date: Sat, 4 Jul 2026 00:28:55 +0200 Subject: [PATCH] security: harden launch defaults and privacy audit --- .../CloudflareSetupTests.swift | 2 +- .../plans/2026-06-27-secure-remote-desktop.md | 2 +- .../2026-06-30-cloudflare-setup-wizard.md | 3 +- gateway/codex_phone_gateway.py | 9 ++-- gateway/test_codex_phone_gateway.py | 41 ++++++++++++++ .../CodexPhoneTests/RemoteDesktopTests.swift | 14 ++--- ops/agents/escalations/security-scan.md | 21 ++++++++ scripts/codepilot-agent-escalate.sh | 4 +- scripts/codepilot-agent-runner.sh | 4 +- scripts/install-codepilot-local-agents.sh | 3 +- scripts/privacy-audit.sh | 53 ++++++++----------- 11 files changed, 107 insertions(+), 49 deletions(-) create mode 100644 ops/agents/escalations/security-scan.md diff --git a/Tests/CodexAccountSwitcherTests/CloudflareSetupTests.swift b/Tests/CodexAccountSwitcherTests/CloudflareSetupTests.swift index c1510bcd..a224b7eb 100644 --- a/Tests/CodexAccountSwitcherTests/CloudflareSetupTests.swift +++ b/Tests/CodexAccountSwitcherTests/CloudflareSetupTests.swift @@ -14,7 +14,7 @@ final class CloudflareSetupTests: XCTestCase { hostname: "codepilot.example.com", tunnelName: "codepilot", tunnelId: "tun_123", - configPath: "/Users/test/.cloudflared/codepilot-config.yaml", + configPath: "/tmp/codepilot-test/.cloudflared/codepilot-config.yaml", launchAgentLabel: "io.codepilot.phone-cloudflared", lastVerifiedAt: nil ) diff --git a/docs/superpowers/plans/2026-06-27-secure-remote-desktop.md b/docs/superpowers/plans/2026-06-27-secure-remote-desktop.md index 661be3e8..d98a3a79 100644 --- a/docs/superpowers/plans/2026-06-27-secure-remote-desktop.md +++ b/docs/superpowers/plans/2026-06-27-secure-remote-desktop.md @@ -632,7 +632,7 @@ Verify pairing approval, Face ID session start, Wi-Fi direct path, cellular TURN Run: ```bash -cd /Users/example/CodePilot/ios +cd ios python3 scripts/dke_ota_build.py build --app codexphone --branch release ``` diff --git a/docs/superpowers/plans/2026-06-30-cloudflare-setup-wizard.md b/docs/superpowers/plans/2026-06-30-cloudflare-setup-wizard.md index 96f3e190..33580987 100644 --- a/docs/superpowers/plans/2026-06-30-cloudflare-setup-wizard.md +++ b/docs/superpowers/plans/2026-06-30-cloudflare-setup-wizard.md @@ -390,7 +390,7 @@ final class CloudflareSetupTests: XCTestCase { hostname: "codepilot.example.com", tunnelName: "codepilot", tunnelId: "tun_123", - configPath: "/Users/test/.cloudflared/codepilot-config.yaml", + configPath: "/tmp/codepilot-test/.cloudflared/codepilot-config.yaml", launchAgentLabel: "io.codepilot.phone-cloudflared", lastVerifiedAt: nil ) @@ -904,4 +904,3 @@ If verification caused small fixes: git add Sources/CodexAccountSwitcher/main.swift Tests/CodexAccountSwitcherTests scripts docs git commit -m "test: verify Cloudflare setup wizard" ``` - diff --git a/gateway/codex_phone_gateway.py b/gateway/codex_phone_gateway.py index f51e15fe..41975ee0 100755 --- a/gateway/codex_phone_gateway.py +++ b/gateway/codex_phone_gateway.py @@ -134,6 +134,7 @@ def __init__( id_factory=None, notification_handler=None, env: dict | None = None, + allow_dangerous: bool = False, ): self.codex_path = codex_path self.cwd = cwd @@ -141,6 +142,7 @@ def __init__( self.id_factory = id_factory or (lambda: str(uuid.uuid4())) self.notification_handler = notification_handler self.env = env + self.allow_dangerous = allow_dangerous self.process = None self.lock = threading.Lock() self.write_lock = threading.Lock() @@ -202,8 +204,8 @@ def close(self): def thread_start(self, cwd: str | None = None, reasoning_effort: str | None = None) -> dict: params = { "cwd": cwd, - "approvalPolicy": "never", - "sandbox": "danger-full-access", + "approvalPolicy": "never" if self.allow_dangerous else "on-request", + "sandbox": "danger-full-access" if self.allow_dangerous else "workspace-write", "threadSource": "user", } if reasoning_effort: @@ -1967,6 +1969,7 @@ def app_server_client(self) -> CodexAppServerClient: id_factory=self.app_server_id_factory, notification_handler=self.handle_app_server_notification, env=env, + allow_dangerous=self.allow_dangerous, ) self._app_server_auth_fingerprint = auth_fingerprint return self._app_server_client @@ -4391,7 +4394,7 @@ def main(): parser.add_argument("--codex-home", default=str(DEFAULT_CODEX_HOME)) parser.add_argument("--codex-path", default=str(DEFAULT_CODEX)) parser.add_argument("--token-file", default=str(DEFAULT_TOKEN_FILE)) - parser.add_argument("--allow-dangerous", action=argparse.BooleanOptionalAction, default=True) + parser.add_argument("--allow-dangerous", action=argparse.BooleanOptionalAction, default=False) args = parser.parse_args() token = read_or_create_token(Path(args.token_file)) diff --git a/gateway/test_codex_phone_gateway.py b/gateway/test_codex_phone_gateway.py index e7ed1fea..27fde163 100644 --- a/gateway/test_codex_phone_gateway.py +++ b/gateway/test_codex_phone_gateway.py @@ -445,6 +445,47 @@ def test_thread_start_sends_reasoning_effort_config(self): self.assertEqual(start["method"], "thread/start") self.assertEqual(start["params"]["config"], {"model_reasoning_effort": "high"}) + def test_thread_start_uses_safe_policy_by_default(self): + process = FakeAppServerProcess([ + json.dumps({"id": "init-id", "result": {"userAgent": "ua", "codexHome": "/tmp/codex", "platformFamily": "unix", "platformOs": "macos"}}) + "\n", + json.dumps({"id": "start-id", "result": {"thread": {"id": "thread-1"}}}) + "\n", + ]) + client = CodexAppServerClient( + codex_path=Path("/usr/local/bin/codex"), + cwd=Path("/tmp/workspace"), + process_factory=lambda args, **kwargs: process, + id_factory=iter(["init-id", "start-id"]).__next__, + ) + + client.start() + client.thread_start("/tmp/workspace") + sent = [json.loads(line) for line in process.stdin.getvalue().splitlines()] + start = sent[2] + + self.assertEqual(start["params"]["approvalPolicy"], "on-request") + self.assertEqual(start["params"]["sandbox"], "workspace-write") + + def test_thread_start_allows_dangerous_mode_when_explicitly_enabled(self): + process = FakeAppServerProcess([ + json.dumps({"id": "init-id", "result": {"userAgent": "ua", "codexHome": "/tmp/codex", "platformFamily": "unix", "platformOs": "macos"}}) + "\n", + json.dumps({"id": "start-id", "result": {"thread": {"id": "thread-1"}}}) + "\n", + ]) + client = CodexAppServerClient( + codex_path=Path("/usr/local/bin/codex"), + cwd=Path("/tmp/workspace"), + process_factory=lambda args, **kwargs: process, + id_factory=iter(["init-id", "start-id"]).__next__, + allow_dangerous=True, + ) + + client.start() + client.thread_start("/tmp/workspace") + sent = [json.loads(line) for line in process.stdin.getvalue().splitlines()] + start = sent[2] + + self.assertEqual(start["params"]["approvalPolicy"], "never") + self.assertEqual(start["params"]["sandbox"], "danger-full-access") + def test_turn_start_sends_reasoning_effort(self): process = FakeAppServerProcess([ json.dumps({"id": "init-id", "result": {"userAgent": "ua", "codexHome": "/tmp/codex", "platformFamily": "unix", "platformOs": "macos"}}) + "\n", diff --git a/ios/CodexPhone/CodexPhoneTests/RemoteDesktopTests.swift b/ios/CodexPhone/CodexPhoneTests/RemoteDesktopTests.swift index 564946b8..4704d1c6 100644 --- a/ios/CodexPhone/CodexPhoneTests/RemoteDesktopTests.swift +++ b/ios/CodexPhone/CodexPhoneTests/RemoteDesktopTests.swift @@ -69,28 +69,28 @@ final class RemoteDesktopTests: XCTestCase { } func testRemoteFilePathAcceptsCustomPreviewURL() throws { - let url = try XCTUnwrap(remoteFilePreviewURL(path: "/Users/example/CodePilot/README.md")) + let url = try XCTUnwrap(remoteFilePreviewURL(path: "/tmp/codepilot-sample/README.md")) - XCTAssertEqual(remoteFilePath(from: url), "/Users/example/CodePilot/README.md") + XCTAssertEqual(remoteFilePath(from: url), "/tmp/codepilot-sample/README.md") } func testRemoteFilePathAcceptsMarkdownAbsolutePathURL() throws { - let url = try XCTUnwrap(URL(string: "/Users/example/CodePilot/docs/superpowers/plans/2026-07-01-codepilot-launch-agent-system.md")) + let url = try XCTUnwrap(URL(string: "/tmp/codepilot-sample/docs/superpowers/plans/2026-07-01-codepilot-launch-agent-system.md")) XCTAssertEqual( remoteFilePath(from: url), - "/Users/example/CodePilot/docs/superpowers/plans/2026-07-01-codepilot-launch-agent-system.md" + "/tmp/codepilot-sample/docs/superpowers/plans/2026-07-01-codepilot-launch-agent-system.md" ) } func testRemoteFilePathAcceptsFileURLAndStripsLineSuffix() throws { - let url = URL(fileURLWithPath: "/Users/example/CodePilot/Sources/App.swift:42") + let url = URL(fileURLWithPath: "/tmp/codepilot-sample/Sources/App.swift:42") - XCTAssertEqual(remoteFilePath(from: url), "/Users/example/CodePilot/Sources/App.swift") + XCTAssertEqual(remoteFilePath(from: url), "/tmp/codepilot-sample/Sources/App.swift") } func testRemoteFilePathRejectsWebURLs() throws { - let url = try XCTUnwrap(URL(string: "https://example.com/Users/example/README.md")) + let url = try XCTUnwrap(URL(string: "https://example.com/tmp/codepilot-sample/README.md")) XCTAssertNil(remoteFilePath(from: url)) } diff --git a/ops/agents/escalations/security-scan.md b/ops/agents/escalations/security-scan.md new file mode 100644 index 00000000..2eb05a5e --- /dev/null +++ b/ops/agents/escalations/security-scan.md @@ -0,0 +1,21 @@ +# CodePilot Security Scan Escalation + +Maintainer intervention is needed before public launch for these product-boundary decisions: + +1. Authenticated arbitrary file download remains enabled. + - Surface: `gateway/codex_phone_gateway.py` exposes `/api/files/download?path=...`. + - Current behavior: any client with the gateway bearer token can request any absolute file path readable by the gateway process. + - Risk: a leaked or phished gateway token becomes direct local file disclosure, independent of Codex approval/sandbox controls. + - Decision needed: define the allowed file scope for iPhone previews, such as uploaded files, explicit per-thread workspace roots, or short-lived file-preview grants. + +2. Localhost proxy sessions are bearerless capability URLs. + - Surface: `POST /api/local-web/sessions` requires the bearer token, but the returned `/api/local-web//...` URL is fetched without bearer auth. + - Current behavior: the random session id gates access for the session lifetime. + - Risk: anyone who obtains a live session URL can proxy GET requests to the selected loopback port through the public gateway until expiry. + - Decision needed: decide whether this is acceptable for WebView compatibility, should have a shorter lifetime, or should move to an authenticated browser/proxy design. + +Local hardening already applied in this branch: + +- Removed public embedded private identifier patterns from the privacy audit and made local denylist patterns opt-in via `CODEPILOT_PRIVACY_PATTERNS_FILE`. +- Removed machine-specific default repository paths from local agent scripts. +- Changed gateway-created Codex app-server threads to use safe approval/sandbox settings unless dangerous mode is explicitly enabled. diff --git a/scripts/codepilot-agent-escalate.sh b/scripts/codepilot-agent-escalate.sh index ce607a70..9354ce00 100755 --- a/scripts/codepilot-agent-escalate.sh +++ b/scripts/codepilot-agent-escalate.sh @@ -1,7 +1,9 @@ #!/usr/bin/env zsh set -euo pipefail -ROOT="${CODEPILOT_REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DEFAULT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +ROOT="${CODEPILOT_REPO_ROOT:-$DEFAULT_ROOT}" STATE_DIR="${CODEPILOT_AGENT_STATE_DIR:-$HOME/.codex-account-switcher/agents}" THREAD_ID_FILE="$STATE_DIR/thread-id" CODEX_BIN="${CODEPILOT_CODEX_BIN:-codex}" diff --git a/scripts/codepilot-agent-runner.sh b/scripts/codepilot-agent-runner.sh index 289a231e..e06d20c7 100755 --- a/scripts/codepilot-agent-runner.sh +++ b/scripts/codepilot-agent-runner.sh @@ -1,7 +1,9 @@ #!/usr/bin/env zsh set -euo pipefail -ROOT="${CODEPILOT_REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DEFAULT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +ROOT="${CODEPILOT_REPO_ROOT:-$DEFAULT_ROOT}" JOB="${1:-}" if [[ -z "$JOB" ]]; then diff --git a/scripts/install-codepilot-local-agents.sh b/scripts/install-codepilot-local-agents.sh index 006485cc..0a45c0da 100755 --- a/scripts/install-codepilot-local-agents.sh +++ b/scripts/install-codepilot-local-agents.sh @@ -1,7 +1,8 @@ #!/usr/bin/env zsh set -euo pipefail -ROOT="${CODEPILOT_REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="${CODEPILOT_REPO_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" PLIST_DIR="$HOME/Library/LaunchAgents" RUNNER="$ROOT/scripts/codepilot-agent-runner.sh" diff --git a/scripts/privacy-audit.sh b/scripts/privacy-audit.sh index 087d1ecc..11557ad3 100755 --- a/scripts/privacy-audit.sh +++ b/scripts/privacy-audit.sh @@ -9,32 +9,21 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 2 fi -pattern="$(python3 - <<'PY' -entries = [ - "546f6e79", - "746f6e79", - "537072656e6b656c696e67", - "737072656e6b656c696e67", - "686f6d65736572766572", - "686f6d655c2e686f6d65", - "4d6163206d696e69", - "6d6163206d696e69", - "444b45", - "64652d6b6c65696e65", - "65656b686f6f726e", - "636f6d5c2e746f6e79", - "636f5c2e737072656e6b656c696e67", - "394b3734443657324252", - "514836354232", - "515a334d364137324747", - "70726f7065726c69", - "676d61696c", - "69636c6f7564", - "40737072656e6b656c696e67", -] -print("|".join(bytes.fromhex(entry).decode("utf-8") for entry in entries)) -PY -)" +private_patterns_file="${CODEPILOT_PRIVACY_PATTERNS_FILE:-$HOME/.codepilot-privacy-patterns}" +users_dir_pattern="/$(printf %s Users)/[^[:space:]\"']+" +generic_private_patterns=( + "$users_dir_pattern" + '[A-Za-z][A-Za-z0-9._%+-]*@[A-Za-z][A-Za-z0-9.-]*\.[A-Za-z]{2,}' +) + +pattern="$(IFS='|'; echo "${generic_private_patterns[*]}")" + +if [[ -f "$private_patterns_file" ]]; then + while IFS= read -r private_pattern; do + [[ -n "$private_pattern" && "${private_pattern:0:1}" != "#" ]] || continue + pattern="$pattern|$private_pattern" + done < "$private_patterns_file" +fi tracked_files=("${(@f)$(git ls-files)}") audit_files=() @@ -49,13 +38,13 @@ if git grep -n -I -E "$pattern" -- "${audit_files[@]}"; then fi secret_patterns=( - 'ghp_[A-Za-z0-9_]+' - 'github_pat_[A-Za-z0-9_]+' - 'sk-[A-Za-z0-9]{20,}' - '-----BEGIN (RSA|OPENSSH|PRIVATE) KEY' + "g""hp_[A-Za-z0-9_]+" + "github_""pat_[A-Za-z0-9_]+" + "s""k-[A-Za-z0-9]{20,}" + "-----BEGIN (RSA|OPENSSH|PRIVATE) ""KEY" 'Bearer [A-Za-z0-9._-]{20,}' - 'client_secret' - 'private_key' + "client_""secret" + "private_""key" ) secret_pattern="$(IFS='|'; echo "${secret_patterns[*]}")"