Skip to content

csp: allow data: and blob: images so Safari and Firefox can draw - #250

Closed
jseifeddine wants to merge 4 commits into
sol1:mainfrom
jseifeddine:fix/csp-img-src
Closed

jseifeddine wants to merge 4 commits into
sol1:mainfrom
jseifeddine:fix/csp-img-src

Conversation

@jseifeddine

@jseifeddine jseifeddine commented Oct 10, 2026 •

Copy link
Copy Markdown

Fixes #249, #251, #252 and #253.

What

Add img-src 'self' data: blob: to the Content-Security-Policy that security_headers puts on every response, move the policy into a documented constant, and add a unit test that fails if img-src ever loses data: or blob: again.

Why

Safari and Firefox rendered SSH and RDP sessions black with only rect fills and the cursor cell visible. The Guacamole client draws each img stream by loading a data:image/...;base64,... URI into an Image on browsers without WebCodecs ImageDecoder. With no img-src directive, default-src 'self' applied and the browser refused every one of those loads. Display.draw() silently skips a failed image, the frame still flushes and sync is still acked, so nothing showed up in rustguac or guacd logs. Chromium decodes straight from the stream bytes through ImageDecoder and never fetches a URL, which is why Chrome was fine on the same server.

The same directive also blocks the remote mouse cursor (cursor: url(data:...) from oncursor) and the blob: image path in Display.drawBlob.

Verification

  • Safari 27.0 against an unpatched build of main: 109 image loads, 109 errors, securitypolicyviolation events with effectiveDirective: "img-src", black framebuffer.
  • Same build with the corrected policy injected by HAProxy: 119/119 loads, 0 violations, framebuffer identical to Chrome over a 400-line scroll.
  • This branch built into the Docker image and opened in Safari directly: text and images render, no violations.
  • cargo test --bin rustguac test_csp passes, cargo fmt --check clean.

Deployed versions

Until this ships, the reverse proxy can replace the header; the full policy is in #249 and in docs/reverse-proxies.md.

Also in this PR

#251: RDP strip on the right. Windows without GFX rounds the requested width down to a multiple of 16 (1710 asked, 1696 given), and the uniform fit in scaleDisplay() left the 14px as bare page. After the uniform fit the display element is now stretched by the remaining fraction on each axis when that is at most 2.5%, and pointer coordinates are divided by the same factors before sendMouseState. Larger gaps are real aspect mismatches and keep the letterbox. Checked against a real Guacamole.Display in headless Chrome: 1710x960 over a 1696x960 framebuffer gives scale(1.00825, 1), an exact fit gives no transform, 1280x1024 in a 1710x960 window keeps the letterbox.

#252: connections table at 1710px. Edit, clone and delete share one cell, and a max-width: 1800px breakpoint tightens page padding, sidebar, cell size and the Connect button. Rendered at 1710x1112 before and after: the row that previously ran off the right edge now fits with ~250px spare and nothing is hidden.

#253: live Connections page, folder default credentials, moving upserts. PUT .../entries/:entry?move=true makes the target folder the entry's only home in the scope (same-named entries elsewhere are deleted and audited), so a NetBox device that changes tenant follows instead of doubling. Folders can lend default credentials per session type (.defaults, admin API and a section in the folder dialog) to entries with none of their own, inherited by subfolders; such entries list as has_credentials + inherited_credentials and connect straight away. GET /api/addressbook/version is a counter a middleware bumps after every successful change through the API, and the page polls it every 3 s, reloading only when it moves (plus a 2-minute unconditional refresh). Verified end to end against OpenBao dev: a move removed the old copy with an audit record, a connect through inherited defaults logged into an SSH target, and the page in Safari followed adds, defaults changes and deletes within one poll with no reload.

The Content-Security-Policy had no img-src, so default-src 'self'
governed images and refused the data: URIs the Guacamole client draws
every tile from on browsers without WebCodecs ImageDecoder. Safari and
Firefox rendered SSH and RDP sessions black with only rect fills and the
cursor cell showing; Chrome decodes straight from the stream and never
fetches a URL, which hid it. The same directive blocks the remote mouse
cursor (cursor: url(data:...)) and blob: image URLs.

Move the policy into a documented constant, add img-src 'self' data:
blob:, and add a unit test that fails if img-src loses either source.
Document the proxy-side header replacement for releases up to v1.10.5.

Fixes sol1#249
Windows RDP without GFX rounds the requested width down to a multiple
of 16, so a 1710-wide window gets a 1696-wide desktop. The uniform fit
in scaleDisplay() then keeps the scale at 1 (the height matches) and
the missing 14px shows as a strip of bare page down the right edge.

After the uniform fit, stretch the display element by the remaining
fraction on each axis when it is at most 2.5%, and divide the same
factors out of pointer coordinates before sendMouseState. A larger gap
is a real aspect mismatch and keeps the letterbox. Multi-monitor
pop-outs reset the fill.

Fixes sol1#251
The 18px table with 20px cell padding and a separate cell each for
edit, clone and delete overran the main column by ~300px on a 1710px
MacBook display, cutting "delete" off the right edge.

Put the three admin actions in one cell, and add a max-width: 1800px
breakpoint on the connections page: 20px page padding, 240-280px
sidebar, 16px cells with tighter padding, 14px headers, a 110px
Connect button and a smaller folder title. The icon-only "open in new
window" button keeps its compact size. Nothing is hidden; at 1710px the
table fits with about 250px to spare.

Fixes sol1#252
Three things kept the Connections page a stale picture of NetBox and
Vault: a device moved between tenants kept its entry in both folders,
a synced entry waited on a five-minute timer for its credentials, and
the page never refreshed until it was reloaded.

PUT .../entries/:entry?move=true deletes same-named entries from every
other folder of the scope after the write, each audited, so an upsert
keyed on a stable name follows its device when the folder changes.

Folders get default credentials: a per-session-type username, password
or private key at <folder>/.defaults, lent at connect time to entries
with no secret of their own, subfolders inheriting per type. Listings
report such entries as has_credentials with inherited_credentials, so
they show Connect from the moment a webhook creates them. Admin API
(GET/PUT/DELETE .../defaults, secrets write-only, audited) and a section
in the folder dialog.

GET /api/addressbook/version is a counter bumped by middleware after
every successful change through the API; it costs no Vault read. The
page polls it every three seconds and reloads folders, entries, the
search index and the credential status only when it moves, plus an
unconditional refresh every two minutes for changes made elsewhere.
Nothing reloads while a dialog is open or the tab is hidden.

Fixes sol1#253
@jseifeddine

Copy link
Copy Markdown
Author

Folded into #255, which carries these four commits as its base and now fixes the same issues (#249, #251, #252, #253) alongside the rest of the UX work. Closing in favour of #255.

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.

Safari/Firefox: SSH and RDP display renders black or glitchy because CSP has no img-src for data: URIs

1 participant