Skip to content

serve-web: blank page when an edge proxy rewrites inline scripts (CSP nonce regression vs 1.130.0) #337226

Closed

Description

@underw8

Does this issue occur when all extensions are disabled?: Yes

  • VS Code Version: 1.138.0 (commit 7debcd0e2acdea1c52de81bf9ee1620444407dda, x64)
  • OS Version: Server: macOS 27.0 (arm64) and Ubuntu 22.04.5 LTS (amd64); Browser: Chrome 153

Summary

code serve-web renders a blank white page when served through an edge proxy that rewrites inline <script> tags — reproducible with Cloudflare Rocket Loader. The workbench CSP blocks the rewritten/injected inline scripts, and the AMD loader never receives its configuration.

This is a regression in behaviour relative to Code 1.130.0, which loads normally under identical proxy settings.

Steps to Reproduce

  1. Start VS Code web:
    code serve-web --host 0.0.0.0 --port 8000 --without-connection-token --accept-server-license-terms
    
  2. Expose it on a hostname behind Cloudflare with Rocket Loader enabled (Speed → Optimization → Rocket Loader).
  3. Open that hostname in a browser.

Rocket Loader rewrites every <script> tag at the edge so the browser ignores it until Rocket Loader restores it, and injects its own loader script:

<!-- origin sends -->
<script nonce="glpx0AH-c7Ijqn9uQvq3pQ" type="text/javascript">
<!-- browser receives -->
<script nonce="glpx0AH-c7Ijqn9uQvq3pQ" type="a843837edca7a2a0e55cf968-text/javascript">

All 6 bootstrap script tags are rewritten this way.

Expected

The workbench loads, as it does on Code 1.130.0 with identical proxy settings.

Actual

Blank white page. document.querySelector('.monaco-workbench') is null; the shell HTML parses but the bootstrap never executes.

Browser console:

Executing inline script violates the following Content Security Policy directive
'script-src 'self' 'unsafe-eval'  blob: 'nonce-eDmj56bClCSuNJCpVxqOyg'
'sha256-daEgfo2VIXpx2Np71KqCCbkeQwv+68vPrx54XRcbdcs='
'sha256-/r7rqQ+yrxt57sxLuQ6AMYcy/lUpvAIzHjIJt/OeLWU=' http://<host>'.
Either the 'unsafe-inline' keyword, a hash
('sha256-HuyBNEnumn/Bw3njx2R0EXAv9HicWHLQQd9NJ9ruyrk='),
or a nonce ('nonce-...') is required to enable inline execution.
The action has been blocked.

[repeated for 'sha256-ivQYuI1zMGd2ljZBmAVZtOcXA1S7nsXTYU46XqYC0NE='
 and 'sha256-yxwQ9j8YGPsfU554CNGiSCW08z5yqDVvuQmssjoPsm8=']

Uncaught Error: Cannot determine URI for module id!

All static assets return HTTP 200 — the server is healthy and this is purely an inline-execution failure:

GET /                                                        200
GET /stable-7debcd0e.../static/out/vs/code/browser/workbench/workbench.js   200
GET /stable-7debcd0e.../static/out/nls.messages.js           200

Version comparison

The CSP script-src emitted by the server differs between versions:

Code 1.130.0 (loads) Code 1.137.0 / 1.138.0 (blank)
nonce 'nonce-1nline-m4p' (static literal) 'nonce-glpx0AH-c7Ijqn9uQvq3pQ' (random per request)
sha256 hashes 5 2

On 1.130.0 the nonce is a fixed string, so inline scripts injected or rewritten by an intermediary — which copy the nonce value out of the surrounding markup — still validate. The longer hash list also covers more inline variants. On 1.137.0+ neither holds.

Notably, the three hashes the browser reports as missing (sha256-HuyBNEnumn/…, sha256-ivQYuI1zMGd2…, sha256-yxwQ9j8YGPsfU…) are present in 1.130.0's script-src and absent from 1.138.0's.

A per-request nonce is the correct posture and I am not arguing for the static nonce to return. Filing this because the failure mode is a silent blank page with no actionable error, and because the change appears to have been made without a fallback for environments where the document is rewritten in transit.

Reproduction scope

Reproduced on two independent machines behind separate Cloudflare Tunnels, and in both code serve-web and code-server:

Code version Result
code serve-web 1.138.0 blank
code-server 4.138.0 (bundles Code 1.138.0) 1.138.0 blank
code-server 4.130.0 (bundles Code 1.130.0) 1.130.0 loads

Both code-server hosts worked on 4.130.0 and broke immediately after upgrading to 4.138.0, with no proxy configuration change in between. code serve-web 1.138.0 fails identically with no code-server in the path, which is why this is filed here rather than at coder/code-server.

Workaround

Disable the script-rewriting feature for the affected hostname. For Cloudflare, a scoped Configuration Rule:

Expression: (http.host eq "code.example.com")
Setting:    Rocket Loader → Off

Confirmed: after applying this, the same 1.138.0 builds load normally — no injected loader script, 0 rewritten <script type="..."> tags, workbench boots, and the CSP nonce remains per-request (server unchanged).

Possible improvements

  1. Surface an actionable error when the bootstrap inline script is blocked, instead of a blank page. Cannot determine URI for module id! gives no indication that CSP or a proxy is involved.
  2. Document the incompatibility with edge script-rewriting optimisations in the serve-web / remote-web docs.

Related downstream reports of the same shape (prior to the nonce change, where the blocked script was the proxy's own): coder/code-server#1451, coder/code-server#4164.

Activity

  1. dmitrivMS commented on Sep 23, 2026

    @dmitrivMS
    Collaborator

    Impacting change: 90c5812

  2. aeschli commented on Sep 28, 2026

    @aeschli
    Contributor

    Thanks for the thorough report. Yes, the change is 90c5812, and your reading of the mechanism is correct.

    That commit fixed a reported security issue in how the workbench HTML is generated. The part relevant to you is how script-src was constructed.

    The old policy had two weaknesses:

    • The nonce was a fixed literal ('nonce-1nline-m4p') — identical in every response and every deployment. A nonce that never changes and is visible in page source provides no protection: anything able to inject markup can read and reuse it, which makes it roughly equivalent to 'unsafe-inline'.
    • The SHA-256 hashes were computed over the fully rendered document, after all dynamic values had been substituted in. Content that reached the page could therefore have its own hash computed and added to script-src — the policy authorized whatever it found, which inverts the purpose of the mechanism.

    The fix replaced the static nonce with a per-request random one and stopped deriving the policy from rendered output. Dropping the inline bootstrap hashes was a side effect — that's the 5 → 2 you spotted, and the three hashes your browser reports as missing are exactly our three inline bootstrap bodies.

    Worth flagging for anyone reading this on an older build: the pre-1.137 behaviour is materially weaker, so 1.130.0 isn't a good place to stay.

    Why Rocket Loader breaks

    It neutralizes each <script> with a placeholder type, injects its own loader, and re-creates the scripts client-side. The re-injected elements don't carry the nonce.

    That isn't an oversight it could easily fix — browsers deliberately hide nonces. When CSP is delivered by header, the nonce content attribute is blanked in the DOM, so page script calling getAttribute('nonce') receives an empty string. That exists specifically to stop content being re-authorized after delivery. Rocket Loader's model requires exactly that capability, so it only works when the policy is permissive enough not to depend on the nonce — which the old rendered-output hash list supplied by accident.

    Why we rather not restore the hashes

    Setting the security question aside, the optimization doesn't buy anything here. Rocket Loader defers JavaScript so page content — text, images, fonts — paints sooner. The workbench is JavaScript; there's no content to paint ahead of it. Deferring the bootstrap just postpones the editor.

    Restoring the hashes would mean widening the policy — hashes authorize those bodies in any response, whereas a nonce authorizes only the response that carried it — in order to support a transformation with no upside on this workload.

    What Cloudflare says

    Their documentation puts the burden on the origin:

    If you have a Content Security Policy (CSP) in place for your domain, you will need to update your headers to support Rocket Loader.

    Cloudflare does not: Modify CSP headers from the origin web server (except when using Zaraz…)

    The documented requirement is script-src 'self' ajax.cloudflare.com, but that only authorizes the loader itself — not the re-injected inline bodies, which is the actual failure. And the 'unsafe-inline' remedy listed for some of their other features is inert here: per CSP Level 2+, 'unsafe-inline' is ignored whenever a nonce or hash is present in the directive. Adding it changes nothing.

    Making Rocket Loader work would therefore require removing the nonce and all hashes and falling back to 'unsafe-inline' — switching off inline-script protection for an application serving source code, credentials and a terminal. We don't want to have that by default, and we don't want to ship it as a flag either.

    Your Configuration Rule is the correct fix and the one we'll document:

    (http.host eq "code.example.com") → Rocket Loader: Off
    

    If someone does want to override the policy regardless, Cloudflare's Response Header Transform Rules can rewrite the CSP at the edge — that's the right layer for it, rather than a server option.

    What we are changing

    Your first suggestion is a fair hit. A blank page whose only symptom is Cannot determine URI for module id! isn't good. I created #338474

  3. underw8 commented on Oct 1, 2026

    @underw8
    Author

    Closing the loop on this one, since the pieces landed in different places.

    (http.host eq "code.example.com") -> Rocket Loader: Off
    

    Nothing outstanding from my side. Happy for this to close whenever suits you, or to stay open until 1.141.0 ships if you would rather track it that way.

    Thanks for the detailed writeup on the nonce and hash reasoning, it made the downstream discussion straightforward.

  4. aeschli commented on Oct 15, 2026

    @aeschli
    Contributor

    Thanks fir the reply, closing the issue

  5. added this to the 1.143.0 milestone on Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions