attacker key binding in the unique profiles.key_id column and | silently black-holing the victim's federation. | */ /** * validateUrl() (called internally by isValidProfileData) resolves the host to * a public IP. Pre-seed the DNS cache so these tests stay deterministic and * network-free while still exercising the host-comparison logic. */ function seedResolvableHost(string $host): void { $key = 'helpers:url:public-ips:'.hash('xxh128', $host); Cache::put($key, ['203.0.113.10'], 3600); } function keyIdActorDoc(array $overrides = []): array { return array_replace_recursive([ 'id' => 'https://joinmastodon.org/users/attacker', 'inbox' => 'https://joinmastodon.org/users/attacker/inbox', 'outbox' => 'https://joinmastodon.org/users/attacker/outbox', 'preferredUsername' => 'attacker', 'publicKey' => [ 'id' => 'https://joinmastodon.org/users/attacker#main-key', 'owner' => 'https://joinmastodon.org/users/attacker', 'publicKeyPem' => "-----BEGIN PUBLIC KEY-----\nattacker\n-----END PUBLIC KEY-----", ], ], $overrides); } beforeEach(function () { seedResolvableHost('joinmastodon.org'); seedResolvableHost('pixelfed.org'); }); it('accepts an actor whose publicKey.id host matches its id host', function () { $res = keyIdActorDoc(); expect(Helpers::isValidProfileData($res, $res['id']))->toBeTrue(); }); it('rejects an actor whose publicKey.id host points at another host', function () { // The plant attempt: attacker actor advertises the victim's keyId URI. $res = keyIdActorDoc([ 'publicKey' => [ 'id' => 'https://pixelfed.org/users/alice#main-key', ], ]); expect(Helpers::isValidProfileData($res, $res['id']))->toBeFalse(); }); it('rejects an actor whose publicKey.id is not a valid url', function () { $res = keyIdActorDoc([ 'publicKey' => [ 'id' => 'not-a-url', ], ]); expect(Helpers::isValidProfileData($res, $res['id']))->toBeFalse(); });