Update InboxWorker, improve handling

pull/7243/head
Daniel Supernault 1 week ago
parent 13a8a255f6
commit 96acfb3d91
No known key found for this signature in database
GPG Key ID: 23740873EE6F76A1

@ -88,7 +88,7 @@ class InboxWorker implements ShouldQueue
) {
return false;
}
if (! isset($bodyDecoded['id'])) {
if (! isset($bodyDecoded['id']) || ! isset($bodyDecoded['actor'])) {
return false;
}
$signatureData = HttpSignature::parseSignatureHeader($signature);
@ -97,25 +97,28 @@ class InboxWorker implements ShouldQueue
return false;
}
$claimedActor = self::actorUrl($bodyDecoded['actor']);
if (! $claimedActor) {
return false;
}
$keyId = Helpers::validateUrl($signatureData['keyId']);
$id = Helpers::validateUrl($bodyDecoded['id']);
$claimedActor = Helpers::validateUrl($claimedActor);
if (! $keyId || ! $id || ! $claimedActor) {
return false;
}
$keyDomain = parse_url($keyId, PHP_URL_HOST);
$idDomain = parse_url($id, PHP_URL_HOST);
$actorDomain = parse_url($payload['actor'] ?? '', PHP_URL_HOST);
$actorDomain = parse_url($claimedActor, PHP_URL_HOST);
if (
isset($bodyDecoded['object'])
&& is_array($bodyDecoded['object'])
&& isset($bodyDecoded['object']['attributedTo'])
) {
$attr = Helpers::pluckval($bodyDecoded['object']['attributedTo']);
if (is_array($attr)) {
if (isset($attr['id'])) {
$attr = $attr['id'];
} else {
$attr = '';
}
}
if (parse_url($attr, PHP_URL_HOST) !== $keyDomain) {
$attr = self::actorUrl($bodyDecoded['object']['attributedTo']);
if (! $attr || parse_url($attr, PHP_URL_HOST) !== $keyDomain) {
return false;
}
}
@ -125,22 +128,26 @@ class InboxWorker implements ShouldQueue
) {
return false;
}
$actor = Profile::whereKeyId($keyId)->first();
if (! $actor) {
$actorUrl = Helpers::pluckval($bodyDecoded['actor']);
$actor = Helpers::profileFirstOrNew($actorUrl);
// Resolve the profile that owns the signing key.
$signer = Profile::whereKeyId($keyId)->first();
if (! $signer) {
$signer = Helpers::profileFirstOrNew($claimedActor);
}
if (! $actor) {
if (! $signer) {
return false;
}
// Rebind: the profile resolved by keyId must belong to the keyId host.
// This rejects a poisoned or stale row whose remote_url host differs
// from the request's keyId host, so a planted key_id -> attacker key
// binding cannot authenticate.
if (parse_url($actor->remote_url, PHP_URL_HOST) !== $keyDomain) {
// The key owner MUST be the actor the activity claims to be from.
// A same-host check is not enough: every account on a multi-user
// instance shares $keyDomain. This subsumes the old rebind check,
// since a row whose remote_url is on another host can never equal
// $claimedActor.
if (! self::sameActorUrl($signer->remote_url, $claimedActor)) {
return false;
}
$pkey = openssl_pkey_get_public($actor->public_key);
$pkey = openssl_pkey_get_public($signer->public_key);
if (! $pkey) {
return false;
}
@ -152,4 +159,54 @@ class InboxWorker implements ShouldQueue
return false;
}
}
/**
* Extract an actor URL from a string, a {"id": ...} object, or a list.
*/
protected static function actorUrl($val)
{
$val = Helpers::pluckval($val);
if (is_array($val)) {
$val = $val['id'] ?? null;
}
return is_string($val) && $val !== '' ? $val : null;
}
/**
* Exact actor identity match. Scheme and host are case-insensitive,
* path is not, a single trailing slash is ignored. Query and fragment
* are part of the comparison so they cannot be used to alias an actor.
*/
protected static function sameActorUrl($a, $b)
{
$a = self::normalizeUrl($a);
$b = self::normalizeUrl($b);
return $a !== null && $b !== null && $a === $b;
}
protected static function normalizeUrl($url)
{
if (! is_string($url) || $url === '') {
return null;
}
$p = parse_url($url);
if (! $p || empty($p['scheme']) || empty($p['host'])) {
return null;
}
$out = strtolower($p['scheme']).'://'.strtolower($p['host']);
if (isset($p['port'])) {
$out .= ':'.$p['port'];
}
$out .= rtrim($p['path'] ?? '', '/');
if (isset($p['query'])) {
$out .= '?'.$p['query'];
}
if (isset($p['fragment'])) {
$out .= '#'.$p['fragment'];
}
return $out;
}
}

Loading…
Cancel
Save