Skip to content

Prebid external bundle: verify proxied bytes against external_bundle_sha256 and fail loudly on mismatch (currently ships a browser-blocked bundle silently) #941

Description

@aram356

Summary

When the Prebid external_bundle_url serves bytes that don't match the configured external_bundle_sha256 / external_bundle_sri, Trusted Server proxies the mismatched bundle anyway with a 200, stamps it with the config-derived ?v=/ETag, and injects the config-derived integrity="sha384-…" into the <script> tag. The browser then blocks the script via Subresource Integrity, Prebid.js never initializes, pbjs.que is never drained, requestBids never runs — no auction, no ads — and nothing on the server side reports a problem.

This is a silent, total Prebid outage that presents as "ads don't render / we don't call the auction", with the only signal buried in the browser console (Failed to find a valid digest in the 'integrity' attribute … the resource has been blocked). TS should not fail silently here — it should detect the mismatch and return an error.

Mechanism

  • The browser integrity= value comes straight from config (external_bundle_sri), injected verbatim in external_bundle_script_tagprebid.rs:701-711.
  • The ?v= cache key comes from external_bundle_sha256prebid.rs:694-698.
  • The bundle is proxied from external_bundle_url via proxy_request in handle_external_bundleprebid.rs:832-862 — and the response is returned without hashing/verifying the proxied bytes against external_bundle_sha256.
  • external_bundle_sha256 is only format-validated at config load (validate_external_bundle_sha256, prebid.rs:448); it is never checked against the actual served content. Likewise external_bundle_sri is only format-validated (prebid.rs:534).

So the served bytes and the two configured hashes are fully decoupled. If the deployed bundle at external_bundle_url is rebuilt/replaced without regenerating the config (both hashes are meant to be produced together by build-prebid-external.mjs → the ts CLI, prebid_bundle.rs:492-493), the config drifts and every browser silently blocks Prebid.

Reproduction (observed on a live deployment)

Config carried one build's hashes while external_bundle_url served a different build. Fetching the proxied bundle:

  • Served bundle (real Prebid.js, ~347 KB): actual sha256 = <A>, actual sha384 = <B>
  • Config / injected: external_bundle_sha256 = <C> (used for ?v= and ETag: sha256:<C>), external_bundle_sri = sha384-<D>
  • <A> != <C> and <B> != <D> — yet the response was 200 application/javascript with ETag: sha256:<C> (a label that doesn't match the actual bytes) and integrity="sha384-<D>".

Result: browser SRI check fails → Prebid.js blocked → pbjs.que never drains → no auction.

Expected behavior — do not fail silently

When external_bundle_sha256 (and/or external_bundle_sri) is configured, TS should verify the proxied bundle bytes against it and refuse to serve a mismatched bundle:

  1. In handle_external_bundle, compute the digest of the proxied response body and compare to external_bundle_sha256.
  2. On mismatch: return an error response (e.g. 502/503 with Cache-Control: no-store) instead of the mismatched bytes, and log::error! a clear diagnostic (expected vs actual digest, external_bundle_url). Never serve bytes that won't match the integrity the page advertises.
  3. Optionally derive external_bundle_sri from the same verified bytes rather than trusting an independent config value that can drift, or at least assert external_bundle_sri is consistent with external_bundle_sha256's source at config-load time.

Implementation note

handle_external_bundle currently proxies .with_streaming() (prebid.rs:856), so full-body verification means either buffering the bundle (it's a static, cacheable asset — acceptable) or hashing on the fly and failing the response if the final digest mismatches. Either is preferable to silently shipping a browser-blocked bundle.

Impact

  • Complete client-side Prebid outage (no bids, no auction, no ads) whenever the external bundle and config hashes drift.
  • Extremely hard to diagnose: server returns 200, x-ts-version present, ad stack injected — the only symptom is a browser-console SRI error and an undrained pbjs.que.

Severity

High — a single stale config value silently disables the entire client-side auction in every browser, with no server-side signal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions