Skip to content

Conversation

k1sul1
Copy link
Contributor

@k1sul1 k1sul1 commented Mar 1, 2018

I made endpoints for getting and verifying nonces. I used an arbitrary $_GET['nonce'] parameter for the verify endpoint and wondered why it didn't work. For too long.

https://wordpress.stackexchange.com/questions/295471/nonce-retrieved-from-the-rest-api-is-invalid-and-different-from-nonce-generated/295547

I made endpoints for getting and verifying nonces. I used an arbitrary $_GET['nonce'] parameter for the verify endpoint and wondered why it didn't work. For too long. https://wordpress.stackexchange.com/questions/295471/nonce-retrieved-from-the-rest-api-is-invalid-and-different-from-nonce-generated/295547
@k1sul1
Copy link
Contributor Author

k1sul1 commented Mar 1, 2018

Related to #19

I like the wording of #19 (comment), but I think that the Authentication page requires a separate paragraph on this.

@rmccue
Copy link
Member

rmccue commented Mar 2, 2018

I made endpoints for getting and verifying nonces.

Please do not ever do this (generate nonces via the REST API). Nonces are the fundamental protection in WordPress against cross-site request forgery (CSRF), and supplying them via the REST API will break the protection it offers unless you secure the code against these attacks in a different way (which is quite difficult to get right). You should to generate the nonce out-of-band, ideally using wp_localize_script (which works for plugins or themes).

For verifying nonces, you can access /wp/v2/users/me; it will either give you a 401 if you didn't pass the nonce, a 403 (I think) if you pass an invalid nonce, or a 200 and the user resource if the nonce is correct.


The nonce checking code does technically reset the user, but a better way to think about it is that without the nonce, your requests are treated as non-authenticated. The resetting of the user is just the way we accomplish that. We can certainly clarify this, as I do think it can be confusing right now.

@k1sul1
Copy link
Contributor Author

k1sul1 commented Mar 2, 2018

I can't use wp_localize_script, as I'm not creating a plugin or a theme.

I'm building a single page app. I'm going to need the nonces as I'm going to have to create posts from the application frontend and modify existing ones. How should I get the nonce? Other authentication methods such as JWT or OAuth aren't going to work for this case.

And if it's a terrible idea, why isn't that in big bold letters, also on the authentication page? 😉 It's only said that you need nonces, but doesn't help you if you're not building a theme or plugin.

Also, I'm not entirely sure how a site at different origin could use this as an attack vector. The nonce generated is different for every user, except the anonymous user. But anonymous users can't do anything anyway that would've required nonces. When the site at a different origin queries the endpoint, it gets the anonymous user nonce, right?

I get that that site could generate a bunch of post delete links like http://wordpress.local/wp-admin/post.php?post=185&action=trash&_wpnonce=bd5c6d33ea, but the nonce from the endpoint doesn't work, because the action of the nonce is wp_rest.

Third party code from GTM & ads is what I'm actually concerned about. Those have access to the nonce and could basically do whatever they want. If I'm not missing something here, cross-origin sites couldn't possibly have the nonce, unless the user actually sent the nonce to them via parameters or form data?

@rmccue
Copy link
Member

rmccue commented Mar 5, 2018

I'm building a single page app. I'm going to need the nonces as I'm going to have to create posts from the application frontend and modify existing ones. How should I get the nonce? Other authentication methods such as JWT or OAuth aren't going to work for this case.

Fundamentally, cookie authentication is only designed for plugins and themes, and should not be used outside of that. I'd recommend using OAuth personally, but with pre-authorised keys; that is, the user never has to manually authorise the key, so it's essentially invisible to them apart from two redirects.

For single-page apps hosted on the domain that really need to use cookies, I can think of two solutions: either load your single page app via WordPress as a sort of dummy theme (this is how I usually use it, in combination with something like react-wp-scripts), or load the nonce in via a cross-site-secured method.

Your code follows the second pattern, which will somewhat work, but you should avoid doing this via the REST API. The REST API sends cross-origin headers allowing access from any domain by default, so you'll have to undo that sending, and it's possible (although unlikely) that this could break in the future. You may also need to disable things like enveloping and method overrides, which can break cross-origin protection. (Personally, I'd have a rewrite rule that generates a JS script dynamically, and lock that down with additional headers.)

It's only said that you need nonces, but doesn't help you if you're not building a theme or plugin.

You're right, we should clarify that nonces are for authentication within WordPress only. It's one of those things that seems obvious when you're writing the docs, but isn't actually. :)

Also, I'm not entirely sure how a site at different origin could use this as an attack vector. The nonce generated is different for every user, except the anonymous user. But anonymous users can't do anything anyway that would've required nonces. When the site at a different origin queries the endpoint, it gets the anonymous user nonce, right?

I get that that site could generate a bunch of post delete links like http://wordpress.local/wp-admin/post.php?post=185&action=trash&_wpnonce=bd5c6d33ea, but the nonce from the endpoint doesn't work, because the action of the nonce is wp_rest.

Nonces aren't about authentication, they're about intent. You could load up a URL which, unbeknownst to you, uses your authentication in the background due to the lack of intent checks.

For example, if your site is at wordpress.local, with your custom endpoints:

// Get the nonce via the insecure endpoint. const { nonce } = await fetch( 'http://wordpress.local/wp-json/nonce/v1', { credentials: 'include' } ).json(); // Fetch all posts, including private ones. const posts = await fetch( `http://wordpress.local/wp-json/wp/v2/posts?status=any&_wpnonce=${ nonce }`, { credentials: 'include' } ); // Delete all the posts. posts.forEach( post => { fetch( `http://wordpress.local/wp-json/wp/v2/posts/${ post.id }?_wpnonce=${ nonce }`, { credentials: 'include', method: 'DELETE', } ) } ); // Fetch the current user's data, including email. const user = fetch( `http://wordpress.local/wp-json/wp/v2/users/me?_wpnonce=${ nonce }`, { credentials: 'include' } );

In a typical (non-WordPress) setup, cross-origin policy would prevent against this. We intentionally made the decision to allow cross-origin requests in order to allow browser-based apps, but this means we need the second layer of protection (nonces) instead.

@k1sul1
Copy link
Contributor Author

k1sul1 commented Mar 5, 2018

I'll look into if we could use OAuth in the way you described it.

Dynamically created script (using a <script> tag) could work, but would't adding origin checks to the endpoints also work?

// register_rest_route('', '', [ if ($_SERVER['HTTP_ORIGIN'] !== get_site_url()) { return new WP_Error('stop_being_evil', 'Stop being evil', ['status' => 403]); } return [ 'nonce' => wp_create_nonce('wp_rest'), 'user' => $user, ]; // ])

Using a dummy theme comes with the cost of loading WordPress every time, which is what we'd like to skip. index.html is easier to scale.

Right now it looks like https://wp-oauth.com/ provides what we need, which is username & password authentication, so I'm probably going to end up scratching this, but for future reference and so on.

@rmccue
Copy link
Member

rmccue commented Mar 6, 2018

Origin checks are best left to the browser, since they're a fair bit more complex than that. The Access-Control-* headers control the browser's behaviour there, and they're sent automatically by WP. You could override those back to the defaults or something more restrictive, but it's possible we introduce new headers over time and you'd have to adjust to that. I wouldn't trust the API to continue working with that into the future.

@kadamwhite kadamwhite merged commit b45f4cc into WP-API:master Mar 15, 2018
@kadamwhite
Copy link
Contributor

I added a commit to remove the function line reference and to bridge the comment about what happens without a nonce into the prior paragraph. I agree with Ryan's points above but it still feels useful to explicitly call out what happens if you don't pass a nonce with your cookie.

@k1sul1 k1sul1 deleted the patch-1 branch March 16, 2018 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants