From a7fc8f22c1ed287cc380cfd80287886ed6c2875b Mon Sep 17 00:00:00 2001 From: mei23 Date: Sat, 17 Feb 2024 18:26:59 +0900 Subject: [PATCH] fix (backend): validate ActivityPub Content-Type Co-authored-by: naskya --- .../src/remote/activitypub/ap-request.ts | 3 +- .../backend/src/remote/activitypub/request.ts | 81 +++++++++++++------ .../src/remote/activitypub/resolver.ts | 9 +-- .../src/server/file/send-drive-file.ts | 12 ++- 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/packages/backend/src/remote/activitypub/ap-request.ts b/packages/backend/src/remote/activitypub/ap-request.ts index d5a9ec053..89266a356 100644 --- a/packages/backend/src/remote/activitypub/ap-request.ts +++ b/packages/backend/src/remote/activitypub/ap-request.ts @@ -65,7 +65,8 @@ export function createSignedGet(args: { method: "GET", headers: objectAssignWithLcKey( { - Accept: "application/activity+json, application/ld+json", + Accept: + 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', Date: new Date().toUTCString(), Host: new URL(args.url).hostname, }, diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts index 69c97a445..9fc2a8115 100644 --- a/packages/backend/src/remote/activitypub/request.ts +++ b/packages/backend/src/remote/activitypub/request.ts @@ -1,9 +1,10 @@ import config from "@/config/index.js"; import { getUserKeypair } from "@/misc/keypair-store.js"; -import type { User } from "@/models/entities/user.js"; -import { getResponse } from "../../misc/fetch.js"; +import type { User, ILocalUser } from "@/models/entities/user.js"; +import { getResponse } from "@/misc/fetch.js"; import { createSignedPost, createSignedGet } from "./ap-request.js"; -import { apLogger } from "@/remote/activitypub/logger.js"; +import type { Response } from "node-fetch"; +import type { IObject } from "./type.js"; export default async (user: { id: User["id"] }, url: string, object: any) => { const body = JSON.stringify(object); @@ -31,30 +32,64 @@ export default async (user: { id: User["id"] }, url: string, object: any) => { }; /** - * Get AP object with http-signature + * Get ActivityPub object * @param user http-signature user * @param url URL to fetch */ -export async function signedGet(url: string, user: { id: User["id"] }) { - apLogger.debug(`Running signedGet on url: ${url}`); - const keypair = await getUserKeypair(user.id); +export async function apGet(url: string, user?: ILocalUser): Promise { + let res: Response; - const req = createSignedGet({ - key: { - privateKeyPem: keypair.privateKey, - keyId: `${config.url}/users/${user.id}#main-key`, - }, - url, - additionalHeaders: { - "User-Agent": config.userAgent, - }, - }); + if (user != null) { + const keypair = await getUserKeypair(user.id); + const req = createSignedGet({ + key: { + privateKeyPem: keypair.privateKey, + keyId: `${config.url}/users/${user.id}#main-key`, + }, + url, + additionalHeaders: { + "User-Agent": config.userAgent, + }, + }); - const res = await getResponse({ - url, - method: req.request.method, - headers: req.request.headers, - }); + res = await getResponse({ + url, + method: req.request.method, + headers: req.request.headers, + }); + } else { + res = await getResponse({ + url, + method: "GET", + headers: { + Accept: + 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + "User-Agent": config.userAgent, + }, + }); + } - return await res.json(); + const contentType = res.headers.get("content-type"); + if (contentType == null || !validateContentType(contentType)) { + throw new Error("Invalid Content Type"); + } + + if (res.body == null) throw new Error("body is null"); + + const text = await res.text(); + if (text.length > 65536) throw new Error("too big result"); + + return JSON.parse(text) as IObject; +} + +function validateContentType(contentType: string): boolean { + const parts = contentType.split(/\s*;\s*/); + if (parts[0] === "application/activity+json") return true; + if (parts[0] !== "application/ld+json") return false; + return parts + .slice(1) + .some( + (part) => + part.trim() === 'profile="https://www.w3.org/ns/activitystreams"', + ); } diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 4e85bb805..691d3b049 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -1,10 +1,9 @@ import config from "@/config/index.js"; -import { getJson } from "@/misc/fetch.js"; import type { ILocalUser } from "@/models/entities/user.js"; import { getInstanceActor } from "@/services/instance-actor.js"; import { fetchMeta } from "@/misc/backend-rs.js"; import { extractHost, isSelfHost } from "backend-rs"; -import { signedGet } from "./request.js"; +import { apGet } from "./request.js"; import type { IObject, ICollection, IOrderedCollection } from "./type.js"; import { isCollectionOrOrderedCollection, getApId } from "./type.js"; import { Notes, NoteReactions, Polls, Users } from "@/models/index.js"; @@ -114,11 +113,7 @@ export default class Resolver { apLogger.debug("Getting object from remote, authenticated as user:"); apLogger.debug(JSON.stringify(this.user, null, 2)); - const object = ( - this.user - ? await signedGet(value, this.user) - : await getJson(value, "application/activity+json, application/ld+json") - ) as IObject; + const object = await apGet(value, this.user); if ( object == null || diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts index 6c9605d51..071f81278 100644 --- a/packages/backend/src/server/file/send-drive-file.ts +++ b/packages/backend/src/server/file/send-drive-file.ts @@ -161,7 +161,17 @@ export default async function (ctx: Koa.Context) { // When doing a conditional request, we MUST return a "Cache-Control" header // if a normal 200 response would have included. - ctx.set("Cache-Control", "max-age=31536000, immutable"); + if (contentType === "application/octet-stream") { + ctx.vary("Accept"); + ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); + + if (ctx.header.accept?.match(/activity\+json|ld\+json/)) { + ctx.status = 400; + return; + } + } else { + ctx.set("Cache-Control", "max-age=2592000, s-maxage=172800, immutable"); + } if (ctx.fresh) { ctx.status = 304;