Fix for CVE-2024-47779 / GHSA-3jm3-x98c-r34x
parent
8904453bbf
commit
8d7f2b5c13
|
@ -40,12 +40,15 @@ global.addEventListener("fetch", (event: FetchEvent) => {
|
|||
|
||||
// Note: ideally we'd keep the request headers etc, but in practice we can't even see those details.
|
||||
// See https://stackoverflow.com/a/59152482
|
||||
let url = event.request.url;
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// We only intercept v3 download and thumbnail requests as presumably everything else is deliberate.
|
||||
// For example, `/_matrix/media/unstable` or `/_matrix/media/v3/preview_url` are something well within
|
||||
// the control of the application, and appear to be choices made at a higher level than us.
|
||||
if (!url.includes("/_matrix/media/v3/download") && !url.includes("/_matrix/media/v3/thumbnail")) {
|
||||
if (
|
||||
!url.pathname.startsWith("/_matrix/media/v3/download") &&
|
||||
!url.pathname.startsWith("/_matrix/media/v3/thumbnail")
|
||||
) {
|
||||
return; // not a URL we care about
|
||||
}
|
||||
|
||||
|
@ -53,34 +56,42 @@ global.addEventListener("fetch", (event: FetchEvent) => {
|
|||
// later on we need to proxy the request through if it turns out the server doesn't support authentication.
|
||||
event.respondWith(
|
||||
(async (): Promise<Response> => {
|
||||
let accessToken: string | undefined;
|
||||
let auth: { accessToken?: string; homeserver: string } | undefined;
|
||||
try {
|
||||
// Figure out which homeserver we're communicating with
|
||||
const csApi = url.substring(0, url.indexOf("/_matrix/media/v3"));
|
||||
const csApi = url.origin;
|
||||
|
||||
// Add jitter to reduce request spam, particularly to `/versions` on initial page load
|
||||
await new Promise<void>((resolve) => setTimeout(() => resolve(), Math.random() * 10));
|
||||
|
||||
// Locate our access token, and populate the fetchConfig with the authentication header.
|
||||
// Locate the access token and homeserver url
|
||||
// @ts-expect-error - service worker types are not available. See 'fetch' event handler.
|
||||
const client = await global.clients.get(event.clientId);
|
||||
accessToken = await getAccessToken(client);
|
||||
auth = await getAuthData(client);
|
||||
|
||||
// Is this request actually going to the homeserver?
|
||||
const isRequestToHomeServer = url.origin === new URL(auth.homeserver).origin;
|
||||
if (!isRequestToHomeServer) {
|
||||
throw new Error("Request appears to be for media endpoint but wrong homeserver!");
|
||||
}
|
||||
|
||||
// Update or populate the server support map using a (usually) authenticated `/versions` call.
|
||||
await tryUpdateServerSupportMap(csApi, accessToken);
|
||||
await tryUpdateServerSupportMap(csApi, auth.accessToken);
|
||||
|
||||
// If we have server support (and a means of authentication), rewrite the URL to use MSC3916 endpoints.
|
||||
if (serverSupportMap[csApi].supportsAuthedMedia && accessToken) {
|
||||
url = url.replace(/\/media\/v3\/(.*)\//, "/client/v1/media/$1/");
|
||||
if (serverSupportMap[csApi].supportsAuthedMedia && auth.accessToken) {
|
||||
url.href = url.href.replace(/\/media\/v3\/(.*)\//, "/client/v1/media/$1/");
|
||||
} // else by default we make no changes
|
||||
} catch (err) {
|
||||
// In case of some error, we stay safe by not adding the access-token to the request.
|
||||
auth = undefined;
|
||||
console.error("SW: Error in request rewrite.", err);
|
||||
}
|
||||
|
||||
// Add authentication and send the request. We add authentication even if MSC3916 endpoints aren't
|
||||
// being used to ensure patches like this work:
|
||||
// https://github.com/matrix-org/synapse/commit/2390b66bf0ec3ff5ffb0c7333f3c9b239eeb92bb
|
||||
return fetch(url, fetchConfigForToken(accessToken));
|
||||
return fetch(url, fetchConfigForToken(auth?.accessToken));
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
@ -106,35 +117,36 @@ async function tryUpdateServerSupportMap(clientApiUrl: string, accessToken?: str
|
|||
|
||||
// Ideally we'd use the `Client` interface for `client`, but since it's not available (see 'fetch' listener), we use
|
||||
// unknown for now and force-cast it to something close enough later.
|
||||
async function getAccessToken(client: unknown): Promise<string | undefined> {
|
||||
async function getAuthData(client: unknown): Promise<{ accessToken: string; homeserver: string }> {
|
||||
// Access tokens are encrypted at rest, so while we can grab the "access token", we'll need to do work to get the
|
||||
// real thing.
|
||||
const encryptedAccessToken = await idbLoad("account", "mx_access_token");
|
||||
|
||||
// We need to extract a user ID and device ID from localstorage, which means calling WebPlatform for the
|
||||
// read operation. Service workers can't access localstorage.
|
||||
const { userId, deviceId } = await askClientForUserIdParams(client);
|
||||
const { userId, deviceId, homeserver } = await askClientForUserIdParams(client);
|
||||
|
||||
// ... and this is why we need the user ID and device ID: they're index keys for the pickle key table.
|
||||
const pickleKeyData = await idbLoad("pickleKey", [userId, deviceId]);
|
||||
if (pickleKeyData && (!pickleKeyData.encrypted || !pickleKeyData.iv || !pickleKeyData.cryptoKey)) {
|
||||
console.error("SW: Invalid pickle key loaded - ignoring");
|
||||
return undefined;
|
||||
throw new Error("SW: Invalid pickle key loaded - ignoring");
|
||||
}
|
||||
|
||||
// Finally, try decrypting the thing and return that. This may fail, but that's okay.
|
||||
try {
|
||||
const pickleKey = await buildAndEncodePickleKey(pickleKeyData, userId, deviceId);
|
||||
return tryDecryptToken(pickleKey, encryptedAccessToken, ACCESS_TOKEN_IV);
|
||||
const accessToken = await tryDecryptToken(pickleKey, encryptedAccessToken, ACCESS_TOKEN_IV);
|
||||
return { accessToken, homeserver };
|
||||
} catch (e) {
|
||||
console.error("SW: Error decrypting access token.", e);
|
||||
return undefined;
|
||||
throw new Error("SW: Error decrypting access token.", { cause: e });
|
||||
}
|
||||
}
|
||||
|
||||
// Ideally we'd use the `Client` interface for `client`, but since it's not available (see 'fetch' listener), we use
|
||||
// unknown for now and force-cast it to something close enough inside the function.
|
||||
async function askClientForUserIdParams(client: unknown): Promise<{ userId: string; deviceId: string }> {
|
||||
async function askClientForUserIdParams(
|
||||
client: unknown,
|
||||
): Promise<{ userId: string; deviceId: string; homeserver: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Dev note: this uses postMessage, which is a highly insecure channel. postMessage is typically visible to other
|
||||
// tabs, windows, browser extensions, etc, making it far from ideal for sharing sensitive information. This is
|
||||
|
|
|
@ -14,6 +14,7 @@ import { Action } from "matrix-react-sdk/src/dispatcher/actions";
|
|||
import { CheckUpdatesPayload } from "matrix-react-sdk/src/dispatcher/payloads/CheckUpdatesPayload";
|
||||
import UAParser from "ua-parser-js";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixClientPeg } from "matrix-react-sdk/src/MatrixClientPeg";
|
||||
|
||||
import VectorBasePlatform from "./VectorBasePlatform";
|
||||
import { parseQs } from "../url_utils";
|
||||
|
@ -62,10 +63,12 @@ export default class WebPlatform extends VectorBasePlatform {
|
|||
if (event.data?.["type"] === "userinfo" && event.data?.["responseKey"]) {
|
||||
const userId = localStorage.getItem("mx_user_id");
|
||||
const deviceId = localStorage.getItem("mx_device_id");
|
||||
const homeserver = MatrixClientPeg.get()?.getHomeserverUrl();
|
||||
event.source!.postMessage({
|
||||
responseKey: event.data["responseKey"],
|
||||
userId,
|
||||
deviceId,
|
||||
homeserver,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
Loading…
Reference in New Issue