Changeset 9910ba1 in Klonkt


Ignore:
Timestamp:
06/28/2026 01:34:06 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
73abbfd
Parents:
cca2e89
Message:

fix(fedi): verify HTTP signatures behind a Host-rewriting proxy

verifyRequest reconstructed the signing string's host line from the raw Host header, which a
reverse proxy that doesn't preserve Host (Apache .htaccess [P] -> backend sees localhost:3000)
makes wrong -> every signed Follow/Like/etc. from another server was rejected as unsigned/invalid
on a proxied instance. Now it tries each candidate host (PUBLIC_BASE_URL host, X-Forwarded-Host,
raw Host) and accepts if the signature verifies against any (an attacker can't forge a match).
Also normalises a leading in the request-target.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    rcca2e89 r9910ba1  
    603603  if (!pem) return null;
    604604  const hs = (p.headers || '(request-target) host date').split(/\s+/);
    605   const line = hs.map((h) => h === '(request-target)'
    606     ? `(request-target): ${req.method.toLowerCase()} ${req.originalUrl}`
    607     : `${h}: ${req.headers[h] || ''}`).join('\n');
     605  // Behind a reverse proxy the raw Host header is the backend bind (e.g. localhost:3000, when
     606  // the proxy doesn't preserve it — Apache .htaccess [P] proxying), but the sender signed the
     607  // HTTP-Signature over the PUBLIC host. Try each candidate host (the configured PUBLIC_BASE_URL
     608  // host, the proxy's X-Forwarded-Host, and the raw Host) and accept if the signature verifies
     609  // against any. An attacker can't forge a match (no private key), so this only rescues the
     610  // legitimate proxied case. Also normalise a leading double-slash in the request-target.
     611  let _pubHost = null;
     612  if (process.env.PUBLIC_BASE_URL) { try { _pubHost = new URL(process.env.PUBLIC_BASE_URL).host; } catch { /* ignore */ } }
     613  const _hosts = [...new Set([_pubHost, req.headers['x-forwarded-host'], req.headers['host']].filter(Boolean))];
     614  const _target = `${req.method.toLowerCase()} ${String(req.originalUrl || '').replace(/^\/{2,}/, '/')}`;
     615  const _sig = Buffer.from(p.signature, 'base64');
    608616  let ok = false;
    609   try { ok = crypto.verify('sha256', Buffer.from(line), pem, Buffer.from(p.signature, 'base64')); } catch { ok = false; }
     617  for (const _h of _hosts) {
     618    const line = hs.map((x) => x === '(request-target)'
     619      ? `(request-target): ${_target}`
     620      : x === 'host' ? `host: ${_h}`
     621      : `${x}: ${req.headers[x] || ''}`).join('\n');
     622    try { if (crypto.verify('sha256', Buffer.from(line), pem, _sig)) { ok = true; break; } } catch { /* try next host */ }
     623  }
    610624  if (ok && hs.includes('digest') && req.rawBody) {
    611625    const exp = 'SHA-256=' + crypto.createHash('sha256').update(req.rawBody).digest('base64');
Note: See TracChangeset for help on using the changeset viewer.