feat: security, cross-check key ownership against received actor

This commit is contained in:
Julian Lam
2024-02-21 13:43:56 -05:00
parent ed4ccbfccc
commit a94341f489
3 changed files with 32 additions and 8 deletions

View File

@@ -31,6 +31,7 @@ Actors.assert = async (ids, options = {}) => {
winston.verbose(`[activitypub/actors] Asserting ${ids.length} actor(s)`); winston.verbose(`[activitypub/actors] Asserting ${ids.length} actor(s)`);
const followersUrlMap = new Map(); const followersUrlMap = new Map();
const pubKeysMap = new Map();
const actors = await Promise.all(ids.map(async (id) => { const actors = await Promise.all(ids.map(async (id) => {
try { try {
winston.verbose(`[activitypub/actors] Processing ${id}`); winston.verbose(`[activitypub/actors] Processing ${id}`);
@@ -58,6 +59,9 @@ Actors.assert = async (ids, options = {}) => {
followersUrlMap.set(actor.followers, actor.id); followersUrlMap.set(actor.followers, actor.id);
} }
// Public keys
pubKeysMap.set(actor.id, actor.publicKey);
return actor; return actor;
} catch (e) { } catch (e) {
return null; return null;
@@ -68,13 +72,14 @@ Actors.assert = async (ids, options = {}) => {
const profiles = await activitypub.mocks.profile(actors); const profiles = await activitypub.mocks.profile(actors);
const now = Date.now(); const now = Date.now();
const bulkSet = profiles.map((profile) => { const bulkSet = profiles.reduce((memo, profile) => {
if (!profile) { if (profile) {
return null; const key = `userRemote:${profile.uid}`;
memo.push([key, profile], [`${key}:keys`, pubKeysMap.get(profile.uid)]);
} }
const key = `userRemote:${profile.uid}`;
return [key, profile]; return memo;
}).filter(Boolean); }, []);
if (followersUrlMap.size) { if (followersUrlMap.size) {
bulkSet.push(['followersUrl:uid', Object.fromEntries(followersUrlMap)]); bulkSet.push(['followersUrl:uid', Object.fromEntries(followersUrlMap)]);
} }

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
const db = require('../database');
const meta = require('../meta'); const meta = require('../meta');
const activitypub = require('../activitypub'); const activitypub = require('../activitypub');
@@ -39,11 +40,29 @@ middleware.validate = async function (req, res, next) {
} }
// Sanity-check payload schema // Sanity-check payload schema
const required = ['type']; const required = ['type', 'actor', 'object'];
if (!required.every(prop => req.body.hasOwnProperty(prop))) { if (!required.every(prop => req.body.hasOwnProperty(prop))) {
return res.sendStatus(400); return res.sendStatus(400);
} }
const { actor, object } = req.body;
// Origin checking
const actorHostname = new URL(actor).hostname;
const objectHostname = new URL(typeof object === 'string' ? object : object.id).hostname;
if (actorHostname !== objectHostname) {
return res.sendStatus(403);
}
// Cross-check key ownership against received actor
await activitypub.actors.assert(actor);
const compare = await db.getObjectField(`userRemote:${actor}:keys`, 'id');
const { signature } = req.headers;
const keyId = new Map(signature.split(',').filter(Boolean).map(v => v.split('='))).get('keyId');
if (`"${compare}"` !== keyId) {
return res.sendStatus(403);
}
next(); next();
}; };

View File

@@ -88,7 +88,7 @@ describe('ActivityPub integration', () => {
}); });
describe.only('.resolveId()', () => { describe('.resolveId()', () => {
let url; let url;
let resolved; let resolved;