element-hq / element-hq/element-web

Authenticated media thumbnails/downloads fail in Safari: <img> lacks crossorigin, forcing no-cors and stripping the Service Worker's Authorization header

Open
#34,790 0 comments 0 reactions 0 assignees View on GitHub
A-File-Download A-Media O-Occasional S-Major T-Defect Z-Platform-Specific
Dominant language
TypeScript
Stars
13.5k
Forks
2.8k
PR merge metrics
PR metrics pending

Description

### Steps to reproduce

1. Log into a homeserver with authenticated media enabled (enable_authenticated_media: true), using Safari on macOS.
2. Open any room with multiple avatars/thumbnails (timeline, member list, room list).
3. Open Safari's Web Inspector → Network tab, reload.
4. Observe repeated FetchEvent.respondWith errors in the console for thumbnail/download requests.

For a failing request such as:
https://matrix.example.org/_matrix/media/v3/thumbnail/example.org/?width=60&height=60&method=crop&allow_redirect=true

Network panel shows:
- Status: — (empty)
- Response headers: none at all
- Request headers actually sent:
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-site
(no Authorization header present on the wire)

Replaying the same request with curl but without an Authorization header reproduces a 404 from Synapse (expected per MSC3916). This confirms the request that actually leaves the browser has no bearer token attached, even though sw.js clearly attempts to add one.

### Outcome

Root cause: sw.js correctly retrieves the access token and calls fetch(t, { headers: { Authorization: `Bearer ${token}` } }). However, the triggering element (avatars/thumbnails) does not set the crossorigin attribute. Per spec, // default to mode: "no-cors" unless crossorigin is set on the element itself — and no-cors mode silently strips custom headers like Authorization, tied to the element's crossorigin setting rather than to the SW's own fetch() mode.

WebKit enforces this element-level restriction strictly even for Service-Worker-mediated responses. Same mechanism documented for in WebKit bug 184447 (bugs.webkit.org/show_bug.cgi?id=184447): "Since the load is in no-cors mode... headers expurged no matter whether the URL is cross origin or not [or what the SW's own fetch specifies]." Also see sjoerdlangkemper.nl/2021/01/06/adding-headers-to-image-request-using-service-workers/ and MDN's crossorigin docs.

Chromium/Gecko appear more permissive and let the SW's fetch() mode take effect on the network — this is why the issue is Safari-specific.

Expected: authenticated media should load in Safari, with the SW able to attach Authorization regardless of engine.

Suggested fix: add crossorigin="anonymous" to // elements used for Matrix media so the browser issues a cors-mode request from the outset, letting the SW's Authorization header actually reach the network in WebKit.

Related: #28370, #27759, #28011

### Operating system

macOS

### Browser information

Safari 26.5.2, WebKit/605.1.15

### URL for webapp

_No response_

### Application version

Element version 1.12.25

### Homeserver

Synapse 1.158.0

### Will you send logs?

No

Contributor guide

Open the contributing guide

Research direction

Start by inspecting sw.js and searching the media-rendering code for the img, video, and audio elements used for avatars, thumbnails, and downloads. Reproduce the Safari failure with authenticated media and verify that the affected requests include the crossorigin setting and load successfully with the Authorization header attached.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, frontend, web-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.