Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/changelog/fix-signature-bare-host
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Signed requests from other servers are now checked more strictly, and requests that are not meant for your site are rejected before the sender's key is fetched.
7 changes: 6 additions & 1 deletion includes/functions-url.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,14 @@ function fold_authority( $authority ) {
* @return bool True if the authority names one of this site's hosts, false otherwise.
*/
function is_signature_host( $authority ) {
// The raw value enters the signature base, so it has to be a bare host and port, written exactly so.
if ( get_authority( \sprintf( '//%s', $authority ) ) !== (string) $authority ) {
return false;
}

$authority = fold_authority( $authority );

// An unparseable value folds to nothing and must not match an unparseable entry.
// A host of only dots or brackets folds to nothing and must not match an unparseable entry.
if ( '' === $authority ) {
return false;
}
Expand Down
11 changes: 6 additions & 5 deletions includes/signature/class-http-signature-draft.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,17 @@ public function verify( array $headers, $body = null ) {
return new \WP_Error( 'activitypub_signature', 'No Key ID present.' );
}

$public_key = Remote_Actors::get_public_key( $parsed['keyId'] );
if ( \is_wp_error( $public_key ) ) {
return $public_key;
}

// Read before the key is fetched, so a request made out to another host or out of its time window fails without a lookup.
$signed_data = $this->get_signed_data( $parsed['headers'], $parsed, $headers );
if ( ! $signed_data ) {
return new \WP_Error( 'invalid_signed_data', 'Signed data is invalid or expired.' );
}

$public_key = Remote_Actors::get_public_key( $parsed['keyId'] );
if ( \is_wp_error( $public_key ) ) {
return $public_key;
}

$algorithm = $this->get_signature_algorithm( $parsed, $public_key );
if ( \is_wp_error( $algorithm ) ) {
return $algorithm;
Expand Down
8 changes: 8 additions & 0 deletions tests/phpunit/tests/includes/class-test-functions-url.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ public function signature_host_provider() {
'home host as a subdomain' => array( 'evil.%s', false ),
'empty' => array( '', false ),
'a port without a host' => array( ':8080', false ),
'home host, trailing dot' => array( '%s.', true ),
'userinfo before home host' => array( 'evil.example@%s', false ),
'home host with a path' => array( '%s/inbox', false ),
'home host with a query' => array( '%s?x=1', false ),
'home host with a fragment' => array( '%s#x', false ),
'leading whitespace' => array( ' %s', false ),
'empty port' => array( '%s:', false ),
'port with a leading zero' => array( '%s:08080', false ),
);
}

Expand Down
10 changes: 10 additions & 0 deletions tests/phpunit/tests/includes/class-test-signature.php
Original file line number Diff line number Diff line change
Expand Up @@ -2040,14 +2040,24 @@ public function test_verify_http_signature_rejects_a_foreign_host() {
\update_option( 'activitypub_rfc9421_signature', '0' );
$mock = $this->mock_remote_key();

$lookups = 0;
$count = function ( $pre ) use ( &$lookups ) {
++$lookups;

return $pre;
};
\add_filter( 'activitypub_pre_http_get_remote_object', $count, 5 );

$request = $this->build_replayed_request( 'evil.example', 'evil.example' );
$result = Signature::verify_http_signature( $request );

\remove_filter( 'activitypub_pre_http_get_remote_object', $count, 5 );
\remove_filter( 'activitypub_pre_http_get_remote_object', $mock );
\delete_option( 'activitypub_rfc9421_signature' );

$this->assertWPError( $result, 'A signature made out to another host must not verify, whatever Host claims.' );
$this->assertSame( 'invalid_signed_data', $result->get_error_code() );
$this->assertSame( 0, $lookups, 'The key must not be looked up for a request made out to another host.' );
}

/**
Expand Down
Loading