1
0
Fork 1
mirror of https://example.com synced 2024-11-22 16:46:38 +09:00

fix (backend): validate ActivityPub Content-Type

Co-authored-by: naskya <m@naskya.net>
This commit is contained in:
mei23 2024-02-17 18:26:59 +09:00 committed by naskya
parent 0afd1de14e
commit a7fc8f22c1
Signed by: naskya
GPG key ID: 712D413B3A9FED5C
4 changed files with 73 additions and 32 deletions

View file

@ -65,7 +65,8 @@ export function createSignedGet(args: {
method: "GET", method: "GET",
headers: objectAssignWithLcKey( 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(), Date: new Date().toUTCString(),
Host: new URL(args.url).hostname, Host: new URL(args.url).hostname,
}, },

View file

@ -1,9 +1,10 @@
import config from "@/config/index.js"; import config from "@/config/index.js";
import { getUserKeypair } from "@/misc/keypair-store.js"; import { getUserKeypair } from "@/misc/keypair-store.js";
import type { User } from "@/models/entities/user.js"; import type { User, ILocalUser } from "@/models/entities/user.js";
import { getResponse } from "../../misc/fetch.js"; import { getResponse } from "@/misc/fetch.js";
import { createSignedPost, createSignedGet } from "./ap-request.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) => { export default async (user: { id: User["id"] }, url: string, object: any) => {
const body = JSON.stringify(object); 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 user http-signature user
* @param url URL to fetch * @param url URL to fetch
*/ */
export async function signedGet(url: string, user: { id: User["id"] }) { export async function apGet(url: string, user?: ILocalUser): Promise<IObject> {
apLogger.debug(`Running signedGet on url: ${url}`); let res: Response;
const keypair = await getUserKeypair(user.id);
const req = createSignedGet({ if (user != null) {
key: { const keypair = await getUserKeypair(user.id);
privateKeyPem: keypair.privateKey, const req = createSignedGet({
keyId: `${config.url}/users/${user.id}#main-key`, key: {
}, privateKeyPem: keypair.privateKey,
url, keyId: `${config.url}/users/${user.id}#main-key`,
additionalHeaders: { },
"User-Agent": config.userAgent, url,
}, additionalHeaders: {
}); "User-Agent": config.userAgent,
},
});
const res = await getResponse({ res = await getResponse({
url, url,
method: req.request.method, method: req.request.method,
headers: req.request.headers, 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"',
);
} }

View file

@ -1,10 +1,9 @@
import config from "@/config/index.js"; import config from "@/config/index.js";
import { getJson } from "@/misc/fetch.js";
import type { ILocalUser } from "@/models/entities/user.js"; import type { ILocalUser } from "@/models/entities/user.js";
import { getInstanceActor } from "@/services/instance-actor.js"; import { getInstanceActor } from "@/services/instance-actor.js";
import { fetchMeta } from "@/misc/backend-rs.js"; import { fetchMeta } from "@/misc/backend-rs.js";
import { extractHost, isSelfHost } from "backend-rs"; 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 type { IObject, ICollection, IOrderedCollection } from "./type.js";
import { isCollectionOrOrderedCollection, getApId } from "./type.js"; import { isCollectionOrOrderedCollection, getApId } from "./type.js";
import { Notes, NoteReactions, Polls, Users } from "@/models/index.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("Getting object from remote, authenticated as user:");
apLogger.debug(JSON.stringify(this.user, null, 2)); apLogger.debug(JSON.stringify(this.user, null, 2));
const object = ( const object = await apGet(value, this.user);
this.user
? await signedGet(value, this.user)
: await getJson(value, "application/activity+json, application/ld+json")
) as IObject;
if ( if (
object == null || object == null ||

View file

@ -161,7 +161,17 @@ export default async function (ctx: Koa.Context) {
// When doing a conditional request, we MUST return a "Cache-Control" header // When doing a conditional request, we MUST return a "Cache-Control" header
// if a normal 200 response would have included. // 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) { if (ctx.fresh) {
ctx.status = 304; ctx.status = 304;