feat: restrict access to ap.probe method to registered users, add rate limiting protection

This commit is contained in:
Julian Lam
2025-05-22 14:13:41 -04:00
parent 99234b3f97
commit e70e990a1a
2 changed files with 23 additions and 1 deletions

View File

@@ -27,6 +27,9 @@ const probeCache = ttl({
max: 500, max: 500,
ttl: 1000 * 60 * 60, // 1 hour ttl: 1000 * 60 * 60, // 1 hour
}); });
const probeRateLimit = ttl({
ttl: 1000 * 3, // 3 seconds
});
const ActivityPub = module.exports; const ActivityPub = module.exports;
@@ -506,6 +509,13 @@ ActivityPub.probe = async ({ uid, url }) => {
* - Returns a relative path if already available, true if not, and false otherwise. * - Returns a relative path if already available, true if not, and false otherwise.
*/ */
// Disable on config setting; restrict lookups to HTTPS-enabled URLs only
const { activitypubProbe } = meta.config;
const { protocol } = new URL(url);
if (!activitypubProbe || protocol !== 'https:') {
return false;
}
// Known resources // Known resources
const [isNote, isMessage, isActor, isActorUrl] = await Promise.all([ const [isNote, isMessage, isActor, isActorUrl] = await Promise.all([
posts.exists(url), posts.exists(url),
@@ -541,6 +551,17 @@ ActivityPub.probe = async ({ uid, url }) => {
} }
} }
// Guests not allowed to use expensive logic path
if (!uid) {
return false;
}
// One request allowed every 3 seconds (configured at top)
const limited = probeRateLimit.get(uid);
if (limited) {
return false;
}
// Cached result // Cached result
if (probeCache.has(url)) { if (probeCache.has(url)) {
return probeCache.get(url); return probeCache.get(url);
@@ -572,6 +593,7 @@ ActivityPub.probe = async ({ uid, url }) => {
return false; return false;
} }
try { try {
probeRateLimit.set(uid, true);
return await checkHeader(meta.config.activitypubProbeTimeout || 2000); return await checkHeader(meta.config.activitypubProbeTimeout || 2000);
} catch (e) { } catch (e) {
if (e.name === 'TimeoutError') { if (e.name === 'TimeoutError') {

View File

@@ -31,7 +31,7 @@ Controller.fetch = async (req, res, next) => {
if (typeof result === 'string') { if (typeof result === 'string') {
return helpers.redirect(res, result); return helpers.redirect(res, result);
} else if (result) { } else if (result) {
const { id, type } = await activitypub.get('uid', req.uid || 0, url.href); const { id, type } = await activitypub.get('uid', req.uid, url.href);
switch (true) { switch (true) {
case activitypub._constants.acceptedPostTypes.includes(type): { case activitypub._constants.acceptedPostTypes.includes(type): {
return helpers.redirect(res, `/post/${encodeURIComponent(id)}`); return helpers.redirect(res, `/post/${encodeURIComponent(id)}`);