summaryrefslogtreecommitdiff
path: root/packages/backend/test/unit
diff options
context:
space:
mode:
author饺子w (Yumechi) <35571479+eternal-flame-AD@users.noreply.github.com>2025-02-23 04:21:34 -0600
committerGitHub <noreply@github.com>2025-02-23 19:21:34 +0900
commit25052164c0971497f9177f88446b110e8ca91ce2 (patch)
tree6df09b4f006d9322706d16f650c3e40b7dd7ceba /packages/backend/test/unit
parentignore js-built (#15523) (diff)
downloadmisskey-25052164c0971497f9177f88446b110e8ca91ce2.tar.gz
misskey-25052164c0971497f9177f88446b110e8ca91ce2.tar.bz2
misskey-25052164c0971497f9177f88446b110e8ca91ce2.zip
Merge commit from fork
* fix(backend): Fix an issue where the origin of ActivityPub lookup response was not validated correctly. [GHSA-6w2c-vf6f-xf26](https://github.com/misskey-dev/misskey/security/advisories/GHSA-6w2c-vf6f-xf26) Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * Enhance: Add configuration option to disable all external redirects when responding to an ActivityPub lookup (config.disallowExternalApRedirect) Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * fixup! fix(backend): Fix an issue where the origin of ActivityPub lookup response was not validated correctly. * docs & one edge case Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * apply suggestions Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * remove stale frontend reference to _responseInvalidIdHostNotMatch Signed-off-by: eternal-flame-AD <yume@yumechi.jp> * apply suggestions Signed-off-by: eternal-flame-AD <yume@yumechi.jp> --------- Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
Diffstat (limited to 'packages/backend/test/unit')
-rw-r--r--packages/backend/test/unit/ap-request.ts125
1 files changed, 125 insertions, 0 deletions
diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts
index d3d39240dc..0426de8e19 100644
--- a/packages/backend/test/unit/ap-request.ts
+++ b/packages/backend/test/unit/ap-request.ts
@@ -8,6 +8,8 @@ import httpSignature from '@peertube/http-signature';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
+import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
+import { IObject } from '@/core/activitypub/type.js';
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
return {
@@ -24,6 +26,10 @@ export const buildParsedSignature = (signingString: string, signature: string, a
};
};
+function cartesianProduct<T, U>(a: T[], b: U[]): [T, U][] {
+ return a.flatMap(a => b.map(b => [a, b] as [T, U]));
+}
+
describe('ap-request', () => {
test('createSignedPost with verify', async () => {
const keypair = await genRsaKeyPair();
@@ -58,4 +64,123 @@ describe('ap-request', () => {
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true);
});
+
+ test('rejects non matching domain', () => {
+ assert.doesNotThrow(() => assertActivityMatchesUrls(
+ 'https://alice.example.com/abc',
+ { id: 'https://alice.example.com/abc' } as IObject,
+ [
+ 'https://alice.example.com/abc',
+ ],
+ FetchAllowSoftFailMask.Strict,
+ ), 'validation should pass base case');
+ assert.throws(() => assertActivityMatchesUrls(
+ 'https://alice.example.com/abc',
+ { id: 'https://bob.example.com/abc' } as IObject,
+ [
+ 'https://alice.example.com/abc',
+ ],
+ FetchAllowSoftFailMask.Any,
+ ), 'validation should fail no matter what if the response URL is inconsistent with the object ID');
+
+ // fix issues like threads
+ // https://github.com/misskey-dev/misskey/issues/15039
+ const withOrWithoutWWW = [
+ 'https://alice.example.com/abc',
+ 'https://www.alice.example.com/abc',
+ ];
+
+ cartesianProduct(
+ cartesianProduct(
+ withOrWithoutWWW,
+ withOrWithoutWWW,
+ ),
+ withOrWithoutWWW,
+ ).forEach(([[a, b], c]) => {
+ assert.doesNotThrow(() => assertActivityMatchesUrls(
+ a,
+ { id: b } as IObject,
+ [
+ c,
+ ],
+ FetchAllowSoftFailMask.Strict,
+ ), 'validation should pass with or without www. subdomain');
+ });
+ });
+
+ test('cross origin lookup', () => {
+ assert.doesNotThrow(() => assertActivityMatchesUrls(
+ 'https://alice.example.com/abc',
+ { id: 'https://bob.example.com/abc' } as IObject,
+ [
+ 'https://bob.example.com/abc',
+ ],
+ FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId,
+ ), 'validation should pass if the response is otherwise consistent and cross-origin is allowed');
+ assert.throws(() => assertActivityMatchesUrls(
+ 'https://alice.example.com/abc',
+ { id: 'https://bob.example.com/abc' } as IObject,
+ [
+ 'https://bob.example.com/abc',
+ ],
+ FetchAllowSoftFailMask.Strict,
+ ), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed');
+ });
+
+ test('rejects non-canonical ID', () => {
+ assert.throws(() => assertActivityMatchesUrls(
+ 'https://alice.example.com/@alice',
+ { id: 'https://alice.example.com/users/alice' } as IObject,
+ [
+ 'https://alice.example.com/users/alice'
+ ],
+ FetchAllowSoftFailMask.Strict,
+ ), 'throws if the response ID did not exactly match the expected ID');
+ assert.doesNotThrow(() => assertActivityMatchesUrls(
+ 'https://alice.example.com/@alice',
+ { id: 'https://alice.example.com/users/alice' } as IObject,
+ [
+ 'https://alice.example.com/users/alice',
+ ],
+ FetchAllowSoftFailMask.NonCanonicalId,
+ ), 'does not throw if non-canonical ID is allowed');
+ });
+
+ test('origin relaxed alignment', () => {
+ assert.doesNotThrow(() => assertActivityMatchesUrls(
+ 'https://alice.example.com/abc',
+ { id: 'https://ap.alice.example.com/abc' } as IObject,
+ [
+ 'https://ap.alice.example.com/abc',
+ ],
+ FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
+ ), 'validation should pass if response is a subdomain of the expected origin');
+ assert.throws(() => assertActivityMatchesUrls(
+ 'https://alice.multi-tenant.example.com/abc',
+ { id: 'https://alice.multi-tenant.example.com/abc' } as IObject,
+ [
+ 'https://bob.multi-tenant.example.com/abc',
+ ],
+ FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
+ ), 'validation should fail if response is a disjoint domain of the expected origin');
+ assert.throws(() => assertActivityMatchesUrls(
+ 'https://alice.example.com/abc',
+ { id: 'https://ap.alice.example.com/abc' } as IObject,
+ [
+ 'https://ap.alice.example.com/abc',
+ ],
+ FetchAllowSoftFailMask.Strict,
+ ), 'throws if relaxed origin is forbidden');
+ });
+
+ test('resist HTTP downgrade', () => {
+ assert.throws(() => assertActivityMatchesUrls(
+ 'https://alice.example.com/abc',
+ { id: 'https://alice.example.com/abc' } as IObject,
+ [
+ 'http://alice.example.com/abc',
+ ],
+ FetchAllowSoftFailMask.Strict,
+ ), 'throws if HTTP downgrade is detected');
+ });
});