Skip to content

fix(security): fail-closed CLI signature verification + https/browserstack.com redirect allowlist [DEVA11Y-473] - #46

Merged
Crash0v3rrid3 merged 7 commits into
mainfrom
fix/DEVA11Y-473-fail-closed-checksum
Oct 22, 2026
Merged

Crash0v3rrid3 merged 7 commits into
mainfrom
fix/DEVA11Y-473-fail-closed-checksum

Conversation

@Crash0v3rrid3

@Crash0v3rrid3 Crash0v3rrid3 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Why

The Security retest of DEVA11Y-473 at 806f98cb found two gaps:

  1. The CLI checksum check let the binary run unverified whenever the .sha256 file was missing, unreachable or malformed.
  2. followRedirects() / download() accepted any redirect target, so an http:// or off-domain hop would be followed. URLSession.shared also auto-follows redirects.

The CDN (sdk-assets.browserstack.com) does not and will not publish checksums, so a checksum could never be verified. Security agreed to verify the binary's own code signature instead, and accepted the Linux residual risk with conditions (comment 2471198).

What

f882c50 – redirect allowlist

  • Downloads go through a session whose delegate only follows redirects to https on browserstack.com / *.browserstack.com.
  • The vetoed Location and the final response.url are re-checked as a backstop.
  • The initial URL is still governed by parseOverride() (HTTPS only).

b8046c3 – signature verification, failing closed (replaces the checksum approach in 2e23e29)

  • macOS: /usr/bin/codesign --verify --strict with a Developer ID requirement pinned to Team YQ5FZQ855D.
  • Windows: Get-AuthenticodeSignature must be Valid, with an O=BrowserStack, Inc. signer.
  • Linux: the build is unsigned. The plugin logs the accepted risk and proceeds; follow-up is SDK-6482.
  • Where it runs:
    • on the staged binary, before it is made executable or published;
    • on cache hits: a cached binary that fails is discarded and downloaded again, which also covers caches filled by older, unverified plugin versions;
    • on the final published binary, because publish can keep a copy a concurrent build published.
  • verifyArchiveChecksum, sha256Hex and the sidecar fetch are removed entirely. README updated.

Testing

Requirement check, run against the real 1.62.1 macOS binary:

Binary Result
Genuine ✅ passes
Flipped byte ❌ rejected
Ad-hoc signed ❌ rejected
Signature stripped ❌ rejected
Genuine, requirement pinned to a different team ❌ rejected

End-to-end via swift package … scan (macOS arm64, real network):

Scenario Result
Default path, forced fresh download ✅ verifies and runs the CLI
Cached binary replaced with an ad-hoc-signed copy ✅ rejected, downloaded again, genuine binary (Team YQ5FZQ855D) restored, run succeeds
Override to the unsigned Linux build on macOS ✅ code object is not signed at all, rejected before chmod/publish; good cache untouched, no staging leftovers
Override redirecting off-domain (GitHub → codeload.github.com) ✅ rejected
Override redirecting https → http://sdk-assets.browserstack.com/… ✅ rejected

CI: all checks green, including a11y-scan end-to-end (SwiftPM).

Not tested: the Windows Authenticode path. I had no Windows host and CI has no Windows job. The signer (BrowserStack, Inc., Sectigo Public Code Signing CA R36) was confirmed from the real binary-win-x64.exe certificate table.

Out of scope

  • Launcher scripts (scripts/*/cli.sh): they don't yet verify the binary's signature. Tracked in DEVA11Y-474, per Security's condition 3.
  • Linux verification: blocked until the Linux build is signed or the API serves a digest (SDK-6482 Approach A).

🤖 Generated with Claude Code

Crash0v3rrid3 and others added 2 commits October 16, 2026 15:54
…VA11Y-473]

followRedirects()/followWithGet() returned any Location target, and
URLSession.shared auto-followed every hop, so an https -> http (or
off-domain) redirect would fetch the CLI archive in cleartext or from an
arbitrary host before our code saw the URL.

Route the HEAD/GET resolution and the archive download through a session
whose delegate vetoes any hop that is not https on browserstack.com or a
subdomain, and re-check the vetoed Location / final response URL after
each request as a backstop. The initial URL is still governed by
parseOverride(), so an HTTPS override to a mirror keeps working as long
as it does not redirect off-domain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…[DEVA11Y-473]

verifyArchiveChecksum aborted only on a digest mismatch. A sidecar that
could not be derived, returned non-2xx, failed to fetch, or was not a
64-char hex digest let the archive be extracted and executed unverified,
so blocking or corrupting the .sha256 bypassed the check entirely
(Security retest of DEVA11Y-473 @806f98cb).

Every one of those paths now throws before extraction. The sidecar fetch
also goes through the redirect-guarded session. README updated to
describe the plugin's fail-closed behaviour.

MERGE GATE: sdk-assets.browserstack.com does not publish <asset>.sha256
yet (403 for all six platform binaries as of 2026-10-16), so this commit
makes every plugin run fail until SDK assets ships the sidecars.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Central YAML (base), Organization UI (inherited), Workspace UI (inherited)
  • Review profile: ASSERTIVE
  • Plan: Enterprise
  • Run ID: b03ed64d-4ff9-4959-9b02-b6a1dc19d4f9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Crash0v3rrid3 and others added 2 commits October 21, 2026 15:35
…sum [DEVA11Y-473]

The CDN does not and will not publish <asset>.sha256 sidecars, so the
checksum path could never pass and every run failed. Security accepted
the Linux residual risk on DEVA11Y-473 on condition the checksum path is
removed entirely and the binary's own signature is verified, failing
closed:

- macOS: codesign --verify --strict against a Developer ID requirement
  pinned to BrowserStack's Team ID YQ5FZQ855D.
- Windows: Authenticode status must be Valid with an O=BrowserStack, Inc.
  signer.
- Linux: the build is unsigned; logs the accepted risk and proceeds.

The check runs on the staged binary before chmod/publish, on cache hits
(a cached binary that fails is discarded and re-downloaded, covering
caches filled by older unverified plugin versions), and on the final
published binary, since publish can keep a concurrent build's copy.
verifyArchiveChecksum, sha256Hex and the sidecar fetch are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Crash0v3rrid3 Crash0v3rrid3 changed the title fix(security): fail-closed CLI checksum + https/browserstack.com redirect allowlist [DEVA11Y-473] fix(security): fail-closed CLI signature verification + https/browserstack.com redirect allowlist [DEVA11Y-473] Oct 21, 2026
@Crash0v3rrid3
Crash0v3rrid3 marked this pull request as ready for review October 21, 2026 10:20
@Crash0v3rrid3
Crash0v3rrid3 requested a review from a team as a code owner October 21, 2026 10:20
@Crash0v3rrid3

Copy link
Copy Markdown
Collaborator Author

@rahul-bs @gobind-singh Ready for Security review. This is the rework you asked for on DEVA11Y-473 (comment 2471198):

  • Redirects are only followed to https on browserstack.com.
  • macOS: codesign --verify --strict, pinned to Team YQ5FZQ855D.
  • Windows: Authenticode, pinned to O=BrowserStack, Inc.
  • Both checks fail closed, and the fail-open checksum path has been removed entirely.
  • Linux follows the accepted risk, with SDK-6482 as the follow-up.

The launcher scripts are handled separately under DEVA11Y-474. The Windows path is not exercised in CI (there is no Windows runner); details are in the description.

…checksum CI jobs [DEVA11Y-474]

The bash/zsh/fish launchers verified the CLI against a <asset>.sha256 sidecar
that the CDN never publishes, and proceeded unverified when it was missing or
malformed: the same bypass Security reopened DEVA11Y-473 over.

Replace verify_binary_integrity with verify_binary_signature, mirroring the
SwiftPM plugin (#46): on macOS, `codesign --verify --strict` against a
Developer ID requirement pinned to Team YQ5FZQ855D, run on the extracted
binary before chmod/publish. Failure refuses to run and drops the cached zip
so the next run re-downloads instead of re-extracting the same payload after
a 304. Linux is unsigned and passes through (accepted risk, DEVA11Y-473);
any other OS is refused.

CI: replace the two checksum jobs, which asserted the fail-open behaviour and
waited on sidecars that will never ship, with
tests/launcher-signature/run_tests.sh:
- matrix: each launcher's function accepts the real CLI and refuses
  flipped-byte, ad-hoc-signed, unsigned and missing binaries;
- published: the latest macOS arm64/x64 releases satisfy the Team-pinned
  requirement, so a re-signed release turns CI red first.

Self-update .sha256 files regenerated for the edited launchers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crash0v3rrid3 and others added 2 commits October 21, 2026 16:01
…ate case [DEVA11Y-474]

The suite lifted verify_binary_integrity by name, which no longer exists, and
drove download_binary with OS=macos and unsigned fixtures that the new
signature check refuses before the guard's outcome is observable.

Run the guard cases with OS=linux, where the real verify_binary_signature
passes through by design, so they still execute only real code. Add case 9
(macOS): the real codesign check must refuse an unsigned payload with exit 2,
remove the staged binary and the zip, and leave an existing cached binary
intact. 75/75 locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nature-verification

fix(security): fail-closed signature check in the launchers; replace checksum CI jobs [DEVA11Y-474]
@Crash0v3rrid3
Crash0v3rrid3 merged commit 1db0d23 into main Oct 22, 2026
14 checks passed
Crash0v3rrid3 pushed a commit that referenced this pull request Oct 23, 2026
…erride [DEVA11Y-479][DEVA11Y-488] (#50)

* fix(security): allowlist browserstack.com for the download URL override [DEVA11Y-479][DEVA11Y-488]

parseOverride() now rejects any --download-url / BROWSERSTACK_A11Y_CLI_DOWNLOAD_URL
whose host is not browserstack.com or a subdomain, using the same policy PR #46
applies to redirect hops. The file:// branches in resolveOverrideArtifact() and
download() are removed, and download() refuses any URL outside the policy.

Adds tests/download-url-policy, which lifts parseOverride verbatim and checks
off-domain, userinfo, look-alike, IP, non-https, file:// and bare-path inputs.
Wired into spm-smoke-test.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(security): reject '.' as a CLI cache version [DEVA11Y-479]

extractVersion() accepted a redirect filename ending in '-..zip' as version
'.', which resolves to the cache root itself. Reject it, and cover the
version parser in tests/download-url-policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: drop the download-url-policy job from spm-smoke-test.yml [DEVA11Y-479]

Keeps the PR to the scope of the Security retest. The suite stays in
tests/download-url-policy and runs locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: run tests/download-url-policy in spm-smoke-test.yml [DEVA11Y-479]

Restores the download-url-policy job so the override allowlist is checked
on every PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(security): exercise RedirectPolicyDelegate's redirect callback offline [DEVA11Y-479][DEVA11Y-488]

The suite checked isAllowedRedirectTarget directly but never the delegate that
applies it to every redirect hop, so bypassing the delegate kept every case
green. Lift RedirectPolicyDelegate from the plugin and drive its
willPerformHTTPRedirection callback with an unstarted task: follow allowed
https targets, veto http, off-domain, look-alike and file hops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Nishant Maurya <nmnishant@outlook.in>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants