Skip to content

Commit 7b32d2e

Browse files
committed
Validate callback URLs using a stored version
See #28
1 parent 5f7f71c commit 7b32d2e

File tree

3 files changed

+120
-4
lines changed

3 files changed

+120
-4
lines changed

admin.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ function json_oauth_admin_validate_parameters( $params ) {
105105
}
106106
$valid['description'] = wp_filter_post_kses( $params['description'] );
107107

108+
if ( empty( $params['callback'] ) ) {
109+
return new WP_Error( 'json_oauth_missing_description', __( 'Consumer callback is required and must be a valid URL.' ) );
110+
}
111+
if ( ! empty( $params['callback'] ) ) {
112+
$valid['callback'] = $params['callback'];
113+
}
114+
108115
return $valid;
109116
}
110117

@@ -138,6 +145,9 @@ function json_oauth_admin_handle_edit_submit( $consumer ) {
138145
$data = array(
139146
'name' => $params['name'],
140147
'description' => $params['description'],
148+
'meta' => array(
149+
'callback' => $params['callback'],
150+
),
141151
);
142152
$consumer = $result = $authenticator->add_consumer( $data );
143153
}
@@ -149,6 +159,7 @@ function json_oauth_admin_handle_edit_submit( $consumer ) {
149159
'post_content' => $params['description'],
150160
);
151161
$result = wp_update_post( $data, true );
162+
update_post_meta( $consumer->ID, 'callback', wp_slash( $params['callback'] ) );
152163
}
153164

154165
if ( is_wp_error( $result ) ) {
@@ -201,13 +212,14 @@ function json_oauth_admin_edit_page() {
201212
$data = array();
202213

203214
if ( empty( $consumer ) || ! empty( $_POST['_wpnonce'] ) ) {
204-
foreach ( array( 'name', 'description' ) as $key ) {
215+
foreach ( array( 'name', 'description', 'callback' ) as $key ) {
205216
$data[ $key ] = empty( $_POST[ $key ] ) ? '' : wp_unslash( $_POST[ $key ] );
206217
}
207218
}
208219
else {
209220
$data['name'] = $consumer->post_title;
210221
$data['description'] = $consumer->post_content;
222+
$data['callback'] = $consumer->callback;
211223
}
212224

213225
// Header time!
@@ -251,6 +263,17 @@ function json_oauth_admin_edit_page() {
251263
cols="30" rows="5" style="width: 500px"><?php echo esc_textarea( $data['description'] ) ?></textarea>
252264
</td>
253265
</tr>
266+
<tr>
267+
<th scope="row">
268+
<label for="oauth-callback"><?php echo esc_html_x( 'Callback', 'field name' ) ?></label>
269+
</th>
270+
<td>
271+
<input type="text" class="regular-text"
272+
name="callback" id="oauth-callback"
273+
value="<?php echo esc_attr( $data['callback'] ) ?>" />
274+
<p class="description"><?php echo esc_html( "Your application's callback URL. The callback passed with the request token must match the scheme, host, port, and path of this URL." ) ?></p>
275+
</td>
276+
</tr>
254277

255278
<?php if ( ! empty( $consumer ) ): ?>
256279
<tr>

lib/class-wp-rest-oauth1-ui.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ public function handle_callback_redirect( $verifier ) {
152152
$callback = $this->token['callback'];
153153

154154
// Ensure the URL is safe to access
155-
$callback = wp_http_validate_url( $callback );
156-
if ( empty( $callback ) ) {
155+
$authenticator = new WP_REST_OAuth1();
156+
if ( ! $authenticator->check_callback( $callback, $this->token['consumer'] ) ) {
157157
return new WP_Error( 'json_oauth1_invalid_callback', __( 'The callback URL is invalid' ), array( 'status' => 400 ) );
158158
}
159159

lib/class-wp-rest-oauth1.php

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,12 @@ public function generate_request_token( $params ) {
380380
);
381381
$data = apply_filters( 'json_oauth1_request_token_data', $data );
382382
add_option( 'oauth1_request_' . $key, $data, null, 'no' );
383+
if ( ! empty( $params['oauth_callback'] ) ) {
384+
$error = $this->set_request_token_callback( $key, $params['oauth_callback'] );
385+
if ( $error ) {
386+
return $error;
387+
}
388+
}
383389

384390
$data = array(
385391
'oauth_token' => self::urlencode_rfc3986($key),
@@ -395,7 +401,8 @@ public function set_request_token_callback( $key, $callback ) {
395401
return $token;
396402
}
397403

398-
if ( esc_url_raw( $callback ) !== $callback ) {
404+
$consumer = $token['consumer'];
405+
if ( ! $this->validate_callback( $callback ) || ! $this->check_callback( $callback, $consumer ) ) {
399406
return new WP_Error( 'json_oauth1_invalid_callback', __( 'Callback URL is invalid' ) );
400407
}
401408

@@ -404,6 +411,92 @@ public function set_request_token_callback( $key, $callback ) {
404411
return $token['verifier'];
405412
}
406413

414+
/**
415+
* Validate a callback URL.
416+
*
417+
* Based on {@see wp_http_validate_url}, but less restrictive around ports
418+
* and hosts. In particular, it allows any scheme, host or port rather than
419+
* just HTTP with standard ports.
420+
*
421+
* @param string $url URL for the callback.
422+
* @return bool True for a valid callback URL, false otherwise.
423+
*/
424+
protected function validate_callback( $url ) {
425+
if ( strpos( $url, ':' ) === false ) {
426+
return false;
427+
}
428+
429+
$parsed_url = wp_parse_url( $url );
430+
if ( ! $parsed_url || empty( $parsed_url['host'] ) )
431+
return false;
432+
433+
if ( isset( $parsed_url['user'] ) || isset( $parsed_url['pass'] ) )
434+
return false;
435+
436+
if ( false !== strpbrk( $parsed_url['host'], ':#?[]' ) )
437+
return false;
438+
439+
return true;
440+
}
441+
442+
/**
443+
* Check whether a callback is valid for a given consumer.
444+
*
445+
* @param string $url Supplied callback.
446+
* @param int|WP_Post $consumer_id Consumer post ID or object.
447+
* @return bool True if valid, false otherwise.
448+
*/
449+
public function check_callback( $url, $consumer_id ) {
450+
$consumer = get_post( $consumer_id );
451+
if ( empty( $consumer ) || $consumer->post_type !== 'json_consumer' || $consumer->type !== $this->type ) {
452+
return false;
453+
}
454+
455+
$registered = $consumer->callback;
456+
if ( empty( $registered ) ) {
457+
return false;
458+
}
459+
460+
$registered = wp_parse_url( $registered );
461+
$supplied = wp_parse_url( $url );
462+
463+
// Check all components except query and fragment
464+
$parts = array( 'scheme', 'host', 'port', 'user', 'pass', 'path' );
465+
$valid = true;
466+
foreach ( $parts as $part ) {
467+
if ( isset( $registered[ $part ] ) !== isset( $supplied[ $part ] ) ) {
468+
$valid = false;
469+
break;
470+
}
471+
472+
if ( ! isset( $registered[ $part ] ) ) {
473+
continue;
474+
}
475+
476+
if ( $registered[ $part ] !== $supplied[ $part ] ) {
477+
$valid = false;
478+
break;
479+
}
480+
}
481+
482+
/**
483+
* Filter whether a callback is counted as valid.
484+
*
485+
* By default, the URLs must match scheme, host, port, user, pass, and
486+
* path. Query and fragment segments are allowed to be different.
487+
*
488+
* To change this behaviour, filter this value. Note that consumers must
489+
* have a callback registered, even if you relax this restruction. It is
490+
* highly recommended not to change this behaviour, as clients will
491+
* expect the same behaviour across all WP sites.
492+
*
493+
* @param boolean $valid True if the callback URL is valid, false otherwise.
494+
* @param string $url Supplied callback URL.
495+
* @param WP_Post $consumer Consumer post; stored callback saved as `consumer` meta value.
496+
*/
497+
return apply_filters( 'rest_oauth.check_callback', $valid, $url, $consumer );
498+
}
499+
407500
/**
408501
* Authorize a request token
409502
*

0 commit comments

Comments
 (0)