#45933 closed defect (bug) (fixed)
WSODs protection returns incorrect content type for JSON Requests
Reported by: | spacedmonkey | Owned by: | flixos90 |
---|---|---|---|
Milestone: | 5.1 | Priority: | normal |
Severity: | normal | Version: | 5.1 |
Component: | Site Health | Keywords: | servehappy has-patch |
Focuses: | multisite | Cc: |
Description (last modified by )
While testing WSODs protection ( #44458 ), I noticed that on REST API calls that wrong content type is returned. The REST API expects valid json to be returned. Other invalid rest requests, return an error state in the following format.
{
code: "rest_no_route",
message: "No route was found matching the URL and request method",
data: {
status: 404
}
}
This issue is that wp_die
, has go handles for, admin ajax, xmlrpc and html responses. But doesn't check for feed or rest api rests.
Attachments (12)
Change History (59)
This ticket was mentioned in Slack in #core-restapi by spacedmonkey. View the logs.
6 years ago
This ticket was mentioned in Slack in #core-php by spacedmonkey. View the logs.
6 years ago
#5
follow-up:
↓ 6
@
6 years ago
- Keywords has-patch added; needs-patch removed
In 45933.diff I had a first pass at handling REST API requests. However, it proved much much harder than I thought to handle REST requests than I thought. It is extremely hard to workout the request is meant to be REST API. This patch add is_doing_rest
, that tries it's best to guess if the current request.
One of the biggest problems I had when testing this, was the fact that rest_api_loaded
calls die
instead of exit
means that, the shutdown handler were never called.
I am looping @rmccue because we need a little history here.
#6
in reply to:
↑ 5
@
6 years ago
Replying to spacedmonkey:
In 45933.diff I had a first pass at handling REST API requests. However, it proved much much harder than I thought to handle REST requests than I thought. It is extremely hard to workout the request is meant to be REST API. This patch add
is_doing_rest
, that tries it's best to guess if the current request.
The REST_REQUEST
constant is built to answer the question of "is the current top-level request a REST API request?".
The main thing is that knowing whether the current request is a REST request or not is mostly irrelevant, because the REST API is re-entrant. Specifically, REST requests can be dispatched and handled on page loads outside of REST API URLs.
If I do rest_do_request()
inside my header template, is the whole request a REST request? Is the request a REST request while rest_do_request()
is running? Is the answer different inside embedded requests fired in the server?
Generally speaking, we want people to avoid asking "is the parent code of the current code running in a REST API request?" because this introduces disparity between how WordPress operates for every other request and how it operates for the API. This means you'll inevitably end up with code that doesn't fit with an API-first mentality. Avoiding a way to detect this helps to shape behaviour to avoid this.
One of the biggest problems I had when testing this, was the fact that
rest_api_loaded
callsdie
instead ofexit
means that, the shutdown handler were never called.
die
is an alias of exit
in PHP, they both fire shutdown hooks, and they both generate a EXIT opcode, so they are equivalent in every way. It's just a stylistic choice as to which to use, not a behavioural one. I'm not sure how you tested this to see disparity between the two.
#7
follow-up:
↓ 8
@
6 years ago
Thanks @rmccue for getting back to me on this one.
The issue here is, that the REST_REQUEST
is setup until parse_request
filter, this is far late to help for this use case. Take the following use case as example.
- User requests posts from rest api - http://build.wordpress-develop.test/wp-json/wp/v2/posts
- Plugin activated by users, is hooked in at
plugins_loaded
- Plugin has fatal error.
- Users is returned error message from shutdown controller.
As plugins_loaded
is before parse_request
, REST_REQUEST
isn't setup and we can't work out if request was meant to be a REST API request and a html error is displayed. doing_rest
should only be true if the url requested wp-json/wp/v2/posts
. So the logic of it should be more like this.
function wp_doing_rest(){
global $wp;
if ( isset( $wp ) && $wp->query_vars['rest_route'] ) {
return true;
}
return false.
}
But this isn't possible as this $wp->add_query_var( 'rest_route' );
isn't called until init.
Not sure how I saw different behaviours on die / exit change, maybe that wasn't the reason why I saw a different and something else was at play here. I don't know.
#8
in reply to:
↑ 7
@
6 years ago
Replying to spacedmonkey:
The issue here is, that the
REST_REQUEST
is setup untilparse_request
filter, this is far late to help for this use case.
WordPress doesn't know that it's a REST API request until WP's routing is done. There's no real way around this; it's simply too early before parse_request
to tell what the request is.
In the error handler, you could try detecting the Accept
header, and external well-formed requests can send Accept: application/json
. Otherwise, I don't think there's really a solution.
#9
@
6 years ago
Thanks for your feedback @rmccue and @ocean90 . I have modified the patch accordingly.
Allowing changes are in 45933.1.diff.
wp_die
useswp_is_json_request
._json_wp_die_handler
introduced._json_wp_die_handler
handlesWP_Error
being passed.wp_is_json_request
now defects jsonp requests (is another bug I found while looking at this).display_error_template
method, now usesWP_Error
where possible.
I want to get this right with json responses, before looking at XML / feeds.
#10
@
6 years ago
Also worth noting you need to apply the patch on #45958 as well, as they related bugs.
This ticket was mentioned in Slack in #core-php by schlessera. View the logs.
6 years ago
#12
@
6 years ago
Patch 45933.3.diff is feedback from Slack conversion.
cc @schlessera for code review
Let's get this merged now and I will write the same patch for xml return.
#13
@
6 years ago
- Milestone changed from Awaiting Review to 5.1
Adding to 5.1 milestone for consideration.
#15
@
6 years ago
While in #34999 the decision was not to support the REST API specifically in wp_die()
, I think this use-case shows that there is a need to support specifically requested content types.
The patch is looking good, we need to double-ensure though the output works in all formats - we should avoid relying on specific HTML markup here as much as possible.
#16
@
6 years ago
45933.4.diff simplifies the previous patches a bit while following the same approach. Most importantly it includes these changes:
- Get rid of supporting
WP_Error
objects in the JSONwp_die()
handler, and thus simplify its code significantly. Only the default handler supports them, so it is not actually safe passingWP_Error
objects, and in our case does not provide any benefits. - Instead of tying in with the
back_link
argument ofwp_die()
, rely on new argumentslink_url
andlink_text
. Our use-case does not actually use a "back link", so using that naming would be confusing. Furthermore, with new arguments there is no risk whatsoever about backward-compatibility breaks. - Simplify the logic in
WP_Shutdown_Handler
a bit, with a newdisplay_default_error_template()
making it easy to override individual pieces. Sincewp_die()
is inwp-includes/functions.php
, a new check is in place, preventing a potential fatal error early in the loading process. Filters to modify thewp_die()
parameters are introduced withwp_php_error_message
andwp_php_error_args
.
#19
@
6 years ago
- Keywords needs-patch added; has-patch removed
@spacedmonkey Now that these changes are committed, feel free to proceed with a wp_die()
handler for RSS feeds. Thanks for tackling this!
#20
@
6 years ago
There patch as merge has a number issues. These include,
- No handling of
WP_Error
. This may generate fatal errors, so I am going to action this next. - No support for jsonp, which the REST API has.
- No XML handling.
I am unhappy with the state of the current commit, so I am going to work on this and get us in a better place. But the fatal errors will need to fixed before 5.1 release or a revert will be needed.
@
6 years ago
In this patch, a new function called _wp_die_process_input add support for WP_Error and formats the response.
#21
@
6 years ago
- Keywords needs-testing added
I have broken these 3 issues into 3 different diff files, so parts can be merged without holding other parts.
- 45933.5.diff simply add jsonp support to response. This stop invalid json being returned for those using the REST API.
- 45933.6.diff add a new helper function that process values inputted into wp_die and gets values of WP_error and returns formatted array. This means patch makes xmlrpc, json, and admin-ajax all allow wp_error object to be passed to them.
- 45933.7.diff Is a first pass at xml wp die handler. It is very similar to json handler but with some special code to output as xml. I couldn't find a risk free way of using
simplexmlelement
class, so I wrote a little helper function called_wp_wrap_xml
. It would be best if 45933.7.diff goes in, that 45933.6.diff goes in as well, as they are designed to work together.
Looping in @flixos90 and @schlessera for code review.
#22
@
6 years ago
Forgot to mention in 45933.6.diff
had a couple of other changes.
- All call
nocache_headers();
, as no error state should be cached and brings inline with default. - All the wp_die handlers respect status passed in the WP_Error object.
#23
@
6 years ago
In 45933.8.diff
updates 45933.7.diff to check if is_feed
or is_trackback
return true.
#24
@
6 years ago
Thanks for following up on this @spacedmonkey!
Since those are three different patches (for a good reason), they should preferably be on individual tickets, to make the conversation and review easier. I think 45933.7.diff / 45933.8.diff respectively should remain here as they tackle the original problem this ticket points out (we still need to add RSS feed support, which those patches are intended for).
I suggest creating separate tickets for both 45933.5.diff and 45933.6.diff.
#25
@
6 years ago
@flixos90 I agree, we can break the JSONP into another ticket as this is a different bug. However, the other two pieces should say on this ticket. The WP_Error handling fixes an issue created the last commit and the fix should stay original ticket for histical reasons. The WP_error processing and xml handler are so heavy coupled it took me 30 minutes to separate them when making the patch. It is going to confuse the matter by breaking this up into two ticket. But anyone feels strongly about the matter, they can create the tickets.
#26
@
6 years ago
I had a quick look at wp_is_xml_request()
in 45933.8.diff and wonder about this part:
if ( isset( $_SERVER['CONTENT_TYPE'] ) && 'application/json' === $_SERVER['CONTENT_TYPE'] ) { if ( isset( $_SERVER['CONTENT_TYPE'] ) && in_array( $_SERVER['CONTENT_TYPE'], $accepted, true ) ) { return true; } }
Here isset( $_SERVER['CONTENT_TYPE'] )
is checked twice and 'application/json'
is not part of $accepted
:
$accepted = array( 'application/rss+xml', 'application/rss+xml', 'text/xml', 'application/atom+xml', 'application/rdf+xml', );
where the 'application/rss+xml'
is duplicated in $accepted
.
#27
@
6 years ago
- Description modified (diff)
- Summary changed from WSODs protection returns incorrect content type to WSODs protection returns incorrect content type for JSON Requests
#28
@
6 years ago
@flixos90 I have create two new tickets and uploaded patches there.
I have renamed this ticket to relate to just the JSON handler. The patch 45933.6.diff should be handled here, as it is bug created by this ticket.
This ticket was mentioned in Slack in #core-php by spacedmonkey. View the logs.
6 years ago
#30
@
6 years ago
@spacedmonkey Sounds good. Some small issues with 45933.6.diff:
- The code is rather complicated to follow. If the preparing function already handles
$message
and$args
, it should also handle$title
, which also relies on the same process and that preparation is currently hard-coded only in the default wp-die-handler. - If you pass a
WP_Error
withoutstatus
set in$data
, the code will currently break, overriding the original$data
and thus effectively deleting$data['status']
. - The preparing function must never override
$args
because it would also erase additional arguments. It should ensure that things likeback_link
,link_url
andlink_text
are maintained as well. - The function lacks docs.
- Generally, the function's behave would be much more obvious by doing the following:
function _wp_die_process_input( $message, $title, $args ) { // all the logic here return array( $message, $title, $args ); }
This way you could write list( $message, $title, $args ) = _wp_die_process_input( $message, $title, $args )
, done. Straightforward and no more custom processing required by the individual handlers.
#31
@
6 years ago
Thanks for the feedback @flixos90
I have changed some of the stuff in your request but not all.
- There is a lack of docs as the function is still being written, docs are a final stage when design is locked down, not in the patch stage.
- With
_wp_die_process_input
the idea is create a formatted array, that can be used by_json_wp_die_handler
. This format is defined here and looks something like this.
{
code: "rest_no_route",
message: "No route was found matching the URL and request method",
data: {
status: 404
}
}
Going forward, I would like xml handler (see #46026) to be in this same structure but just formatted as xml. So
<error>
<code>rest_no_route</code>
<message>No route was found matching the URL and request method</message>
<data>
<status>404</status>
</data>
</error>
Keeping this structure is important, as it means that, if we copy the existing structure from the rest api, then this error message should work with existing code (in gutenberg) that handles REST API errors. It is important to have a defined error state that doesn't change it's format, as monitor tools and error handling will depend on this exact formatting.
I know the format of the data isn't perfect, I don't love the data array for status and title, but it makes it possible for WP_Error
to pass a title as well as status, which is an improvement.
#32
@
6 years ago
I agree it makes sense to output the JSON data in accordance with how the REST API does it. However, note that wp_is_json_request()
is in no way actually related to the REST API.
More importantly though, let's separate preparing the data passed to the function from actually outputting them. The individual handlers are responsible for the output - _wp_die_process_input()
should, like its name already states, be responsible for processing the input, not processing it for output. That function is part of the "wp_die()
API" (if it can be called that), so its behavior should be in line with the other related functions, which mean consisting of $message
, $title
, and $args
. $args
should consist of the existing "response", "back_link", "link_url", "link_text", plus the new "code". It makes sense to just pass through the three wp_die()
parameters like that for easily understanding that this function simply prepares arguments.
The individual handlers can then use this data in any way they like, for example the JSON handler would need to output the following:
list( $message, $title, $args ) = _wp_die_process_input( $message, $title, $args ); ... echo wp_json_encode( array( 'code' => $args['code'], 'message' => $message, 'data' => array( 'status' => $args['response'], ), ) );
#33
@
6 years ago
In 45933.10.diff I made _wp_die_process_input
much more generic. Now it just processes the values are returns an error.
The only thing that is different from the above idea, is I changed the order of the params of the process function and added an array of errors, as it was required.
This is feeling really close now. Feel happy we could merge this soon after a little testing.
#34
@
6 years ago
- Keywords has-patch added; needs-patch removed
45933.2.diff is a modification of 45933.10.diff that makes the following changes:
- Make
_wp_die_process_input()
more predictable and aligned with other functions: You pass in$message
,$title
, and$args
, and you get back$message
,$title
, and$args
. - Move all input data processing into the new function so that the handlers really only need to worry about the output. For example, the
wp_parse_args()
call and default values for arguments liketext_direction
are now handled there as well (note that_ajax_wp_die_handler()
still contains its own call to ensure the "irregular" default of a 200 response code). The handler itself is then free to choose which of that information to render. - Ensure that additional errors each contain their code, message, and data rather than only their message.
- Ensure that JSON output is formatted coherently with REST API errors, with a
data
key includingstatus
instead ofstatus
in the root.
This way it is ensured that the function calling wp_die()
does not need to know what kind of request is currently being made. Regardless of how/which arguments are passed, they will just work.
#35
follow-up:
↓ 36
@
6 years ago
A few observations regarding the latest patch:
- Line 3375:
$args['code'] = 500;
should be$args['response'] = 500;
- Doing
empty()
checks on strings means that it is not possible to override a string like$args['title']
to be an empty string. Just as well, doingempty()
checks on integers means that it is not possible to override values like$args['code']
to be0
.isset()
orarray_key_exists()
checks would be more explicit and still allow for empty strings and the value0
as a valid input. $args['response']
seems to have both0
(line 3327) as well as500
(line 3375) as default values. If it should be0
, then some of the calls will be wrong, as setting of return code 500 is removed in some places by the patch (line 3024, line 3218, line 3257).'code'
should be an integer but defaults to an empty string (line 3328).'text_direction'
could default to'ltr'
to simplify the code (line 3332).
#36
in reply to:
↑ 35
@
6 years ago
Replying to schlessera:
Thanks for the review!
- Doing
empty()
checks on strings means that it is not possible to override a string like$args['title']
to be an empty string. Just as well, doingempty()
checks on integers means that it is not possible to override values like$args['code']
to be0
.isset()
orarray_key_exists()
checks would be more explicit and still allow for empty strings and the value0
as a valid input.
That is intentional and needed for backward-compatibility. Existing code ensures already that the response code and title are never empty.
$args['response']
seems to have both0
(line 3327) as well as500
(line 3375) as default values. If it should be0
, then some of the calls will be wrong, as setting of return code 500 is removed in some places by the patch (line 3024, line 3218, line 3257).
The value is set to 0 specifically to allow overriding it. For example the WP_Error
should only override the value if it wasn't manually set to something already. Since setting the value to 0 is not supported, we can rely on this. If the default value initially was 500, it would be more complicated to determine whether that 500 is the default or was actually passed.
'text_direction'
could default to'ltr'
to simplify the code (line 3332).
Same here. The logic that sets a default (via is_rtl()
check) should only kick in if no value has passed (as in the value is empty). If 'ltr'
was the default, this would be indistinguishable.
#37
@
6 years ago
- Keywords needs-testing removed
45933.12.diff addresses the bugs pointed out by @schlessera. It also documents the new $code
argument you can pass to wp_die()
(currently only used by the JSON handler).
This ticket was mentioned in Slack in #core-php by flixos90. View the logs.
6 years ago
#41
@
6 years ago
@flixos90 On last thing for _wp_die_process_input
I would check if $args['response']
is an int. Maybe something like this
if ( empty( $args['response'] ) || !is_int( $args['response'] ) ) {
$args['response'] = 500;
}
#43
@
6 years ago
- Resolution fixed deleted
- Status changed from closed to reopened
@flixos90 [44666] has introduced a fatal, looks like the $error
variable is supposed to be something else
E_ERROR: /wp-includes/functions.php:3377 Uncaught Error: Call to a member function get_error_data() on null in /wp-includes/functions.php:3377
https://core.trac.wordpress.org/browser/trunk/src/wp-includes/functions.php?marks=3377#L3355
Related: #34999