Skip to content

Commit 142c0a6

Browse files
committed
Merge remote-tracking branch 'origin/develop' into develop
2 parents b9aacea + 76e653b commit 142c0a6

File tree

8 files changed

+275
-83
lines changed

8 files changed

+275
-83
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
Changes in [34.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.0) (2024-11-12)
2+
====================================================================================================
3+
# Security
4+
- Fixes for [CVE-2024-50336](https://nvd.nist.gov/vuln/detail/CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr).
5+
16
Changes in [34.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.10.0) (2024-11-05)
27
====================================================================================================
38
## 🦖 Deprecations

spec/unit/content-repo.spec.ts

+25-14
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,6 @@ describe("ContentRepo", function () {
6363
);
6464
});
6565

66-
it("should put fragments from mxc:// URIs after any query parameters", function () {
67-
const mxcUri = "mxc://server.name/resourceid#automade";
68-
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
69-
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32#automade",
70-
);
71-
});
72-
73-
it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () {
74-
const mxcUri = "mxc://server.name/resourceid#automade";
75-
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
76-
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
77-
);
78-
});
79-
8066
it("should return an authenticated URL when requested", function () {
8167
const mxcUri = "mxc://server.name/resourceid";
8268
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, true, true)).toEqual(
@@ -98,5 +84,30 @@ describe("ContentRepo", function () {
9884
"/_matrix/client/v1/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
9985
);
10086
});
87+
88+
it("should drop mxc urls with invalid server_name", () => {
89+
const mxcUri = "mxc://server.name:test/foobar";
90+
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
91+
});
92+
93+
it("should drop mxc urls with invalid media_id", () => {
94+
const mxcUri = "mxc://server.name/foobar:test";
95+
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
96+
});
97+
98+
it("should drop mxc urls attempting a path traversal attack", () => {
99+
const mxcUri = "mxc://../../../../foo";
100+
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
101+
});
102+
103+
it("should drop mxc urls attempting to pass query parameters", () => {
104+
const mxcUri = "mxc://server.name/foobar?bar=baz";
105+
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
106+
});
107+
108+
it("should drop mxc urls with too many parts", () => {
109+
const mxcUri = "mxc://server.name/foo//bar";
110+
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
111+
});
101112
});
102113
});

spec/unit/matrixrtc/MatrixRTCSession.spec.ts

+51-1
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,36 @@ describe("MatrixRTCSession", () => {
467467
jest.useRealTimers();
468468
});
469469

470+
it("uses membershipExpiryTimeout from join config", async () => {
471+
const realSetTimeout = setTimeout;
472+
jest.useFakeTimers();
473+
sess!.joinRoomSession([mockFocus], mockFocus, { membershipExpiryTimeout: 60000 });
474+
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
475+
expect(client.sendStateEvent).toHaveBeenCalledWith(
476+
mockRoom!.roomId,
477+
EventType.GroupCallMemberPrefix,
478+
{
479+
memberships: [
480+
{
481+
application: "m.call",
482+
scope: "m.room",
483+
call_id: "",
484+
device_id: "AAAAAAA",
485+
expires: 60000,
486+
expires_ts: Date.now() + 60000,
487+
foci_active: [mockFocus],
488+
489+
membershipID: expect.stringMatching(".*"),
490+
},
491+
],
492+
},
493+
"@alice:example.org",
494+
);
495+
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
496+
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
497+
jest.useRealTimers();
498+
});
499+
470500
describe("non-legacy calls", () => {
471501
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
472502
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
@@ -478,6 +508,19 @@ describe("MatrixRTCSession", () => {
478508

479509
jest.useFakeTimers();
480510

511+
// preparing the delayed disconnect should handle the delay being too long
512+
const sendDelayedStateExceedAttempt = new Promise<void>((resolve) => {
513+
const error = new MatrixError({
514+
"errcode": "M_UNKNOWN",
515+
"org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
516+
"org.matrix.msc4140.max_delay": 7500,
517+
});
518+
sendDelayedStateMock.mockImplementationOnce(() => {
519+
resolve();
520+
return Promise.reject(error);
521+
});
522+
});
523+
481524
// preparing the delayed disconnect should handle ratelimiting
482525
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
483526
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
@@ -511,7 +554,14 @@ describe("MatrixRTCSession", () => {
511554
});
512555
});
513556

514-
sess!.joinRoomSession([activeFocusConfig], activeFocus, { useLegacyMemberEvents: false });
557+
sess!.joinRoomSession([activeFocusConfig], activeFocus, {
558+
useLegacyMemberEvents: false,
559+
membershipServerSideExpiryTimeout: 9000,
560+
});
561+
562+
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 9000);
563+
await sendDelayedStateExceedAttempt.then(); // needed to resolve after the send attempt catches
564+
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 7500);
515565

516566
await sendDelayedStateAttempt;
517567
jest.advanceTimersByTime(5000);

src/content-repo.ts

+35-30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
2+
Copyright 2015 - 2024 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -14,7 +14,22 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { encodeParams } from "./utils.ts";
17+
// Validation based on https://spec.matrix.org/v1.12/appendices/#server-name
18+
// We do not use the validation described in https://spec.matrix.org/v1.12/client-server-api/#security-considerations-5
19+
// as it'd wrongly make all MXCs invalid due to not allowing `[].:` in server names.
20+
const serverNameRegex =
21+
/^(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(?:\[[\dA-Fa-f:.]{2,45}])|(?:[A-Za-z\d\-.]{1,255}))(?::\d{1,5})?$/;
22+
function validateServerName(serverName: string): boolean {
23+
const matches = serverNameRegex.exec(serverName);
24+
return matches?.[0] === serverName;
25+
}
26+
27+
// Validation based on https://spec.matrix.org/v1.12/client-server-api/#security-considerations-5
28+
const mediaIdRegex = /^[\w-]+$/;
29+
function validateMediaId(mediaId: string): boolean {
30+
const matches = mediaIdRegex.exec(mediaId);
31+
return matches?.[0] === mediaId;
32+
}
1833

1934
/**
2035
* Get the HTTP URL for an MXC URI.
@@ -36,7 +51,7 @@ import { encodeParams } from "./utils.ts";
3651
* for authenticated media will *not* be checked - it is the caller's responsibility
3752
* to do so before calling this function. Note also that `useAuthentication`
3853
* implies `allowRedirects`. Defaults to false (unauthenticated endpoints).
39-
* @returns The complete URL to the content.
54+
* @returns The complete URL to the content, may be an empty string if the provided mxc is not valid.
4055
*/
4156
export function getHttpUriForMxc(
4257
baseUrl: string,
@@ -51,14 +66,19 @@ export function getHttpUriForMxc(
5166
if (typeof mxc !== "string" || !mxc) {
5267
return "";
5368
}
54-
if (mxc.indexOf("mxc://") !== 0) {
69+
if (!mxc.startsWith("mxc://")) {
5570
if (allowDirectLinks) {
5671
return mxc;
5772
} else {
5873
return "";
5974
}
6075
}
6176

77+
const [serverName, mediaId, ...rest] = mxc.slice(6).split("/");
78+
if (rest.length > 0 || !validateServerName(serverName) || !validateMediaId(mediaId)) {
79+
return "";
80+
}
81+
6282
if (useAuthentication) {
6383
allowRedirects = true; // per docs (MSC3916 always expects redirects)
6484

@@ -67,46 +87,31 @@ export function getHttpUriForMxc(
6787
// callers, hopefully.
6888
}
6989

70-
let serverAndMediaId = mxc.slice(6); // strips mxc://
7190
let prefix: string;
91+
const isThumbnailRequest = !!width || !!height || !!resizeMethod;
92+
const verb = isThumbnailRequest ? "thumbnail" : "download";
7293
if (useAuthentication) {
73-
prefix = "/_matrix/client/v1/media/download/";
94+
prefix = `/_matrix/client/v1/media/${verb}`;
7495
} else {
75-
prefix = "/_matrix/media/v3/download/";
96+
prefix = `/_matrix/media/v3/${verb}`;
7697
}
77-
const params: Record<string, string> = {};
98+
99+
const url = new URL(`${prefix}/${serverName}/${mediaId}`, baseUrl);
78100

79101
if (width) {
80-
params["width"] = Math.round(width).toString();
102+
url.searchParams.set("width", Math.round(width).toString());
81103
}
82104
if (height) {
83-
params["height"] = Math.round(height).toString();
105+
url.searchParams.set("height", Math.round(height).toString());
84106
}
85107
if (resizeMethod) {
86-
params["method"] = resizeMethod;
87-
}
88-
if (Object.keys(params).length > 0) {
89-
// these are thumbnailing params so they probably want the
90-
// thumbnailing API...
91-
if (useAuthentication) {
92-
prefix = "/_matrix/client/v1/media/thumbnail/";
93-
} else {
94-
prefix = "/_matrix/media/v3/thumbnail/";
95-
}
108+
url.searchParams.set("method", resizeMethod);
96109
}
97110

98111
if (typeof allowRedirects === "boolean") {
99112
// We add this after, so we don't convert everything to a thumbnail request.
100-
params["allow_redirect"] = JSON.stringify(allowRedirects);
101-
}
102-
103-
const fragmentOffset = serverAndMediaId.indexOf("#");
104-
let fragment = "";
105-
if (fragmentOffset >= 0) {
106-
fragment = serverAndMediaId.slice(fragmentOffset);
107-
serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset);
113+
url.searchParams.set("allow_redirect", JSON.stringify(allowRedirects));
108114
}
109115

110-
const urlParams = Object.keys(params).length === 0 ? "" : "?" + encodeParams(params);
111-
return baseUrl + prefix + serverAndMediaId + urlParams + fragment;
116+
return url.href;
112117
}

src/embedded.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export class RoomWidgetClient extends MatrixClient {
162162
data: T,
163163
): Promise<R> => {
164164
try {
165-
return await transportSend<T, R>(action, data);
165+
return await transportSend(action, data);
166166
} catch (error) {
167167
processAndThrow(error);
168168
}
@@ -174,7 +174,7 @@ export class RoomWidgetClient extends MatrixClient {
174174
data: T,
175175
): Promise<R> => {
176176
try {
177-
return await transportSendComplete<T, R>(action, data);
177+
return await transportSendComplete(action, data);
178178
} catch (error) {
179179
processAndThrow(error);
180180
}

0 commit comments

Comments
 (0)