Roundup Tracker - Issues

Issue 2551203

classification
Add support for CORS preflight request
Type: rfe Severity: normal
Components: Web interface, API Versions: devel
process
Status: fixed fixed
:
: rouilj : marcus.priesch, rouilj, schlatterbeck
Priority: normal : patch

Created on 2022-05-04 06:21 by marcus.priesch, last changed 2022-06-07 22:08 by rouilj.

Files
File name Uploaded Description Edit Remove
cors_preflight.patch marcus.priesch, 2022-05-04 06:21
Allow_CORS_Preflight_through_rest_handlers.patch rouilj, 2022-05-08 23:55 Broken. Do not use - Push handling of CORS preflight down into the OPTIONS handlers in the rest code.
Fixed1_Allow_CORS_Preflight_through_rest_handlers.patch rouilj, 2022-05-12 21:40 Sends Access-Control-Allow-Origin and Access-Control-Allow-Credentials for preflight and CORS requests.
Fixed2_Allow_CORS_Preflight_through_rest_handlers.patch rouilj, 2022-06-01 14:38 Clean up test cases, return 400 on origin check failure.
Messages
msg7502 Author: [hidden] (marcus.priesch) Date: 2022-05-04 06:21
upon developing a rest frontend for roundup i came across 
the need for so called CORS preflight requests. 

see https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request

basically this is a "OPTIONS" request that gets sent by the browser when 
you want to do CORS.

in frontend development i have a roundup-server running on port 8080 and 
a node.js server running on port 8081 where the frontend gets 
developed (vue.js with hot module reload etc.)

the web frontend gets loaded from the node.js server at port 8081 (to 
have all the fancy hot reloading working) and communicates with the rest 
api of the roundup server at port 8080 thus forming a nice CORS setup 
where the browser issues CORS preflight requests before any real 
request to ask the server what he is allowed to do.

attached is a patch that i have applied to make this working.

as i am not really deep in the CORS thing i am not sure if my solution 
covers all aspects of this or has any security impacts. 

the patch most likely also needs to be utilized for the xmlrpc frontend 
if it gets accessed from within a webbrowser - can't imagine who wants 
this, but maybe we should support it - but maybe it should be implemented 
for all web requests ? - could make sense to also talk to the html 
frontend directly via CORS from within another webpage.

so the patch is meant as a starting point to discuss how this can be added 
to roundup at all - and what i needed to start hacking with the rest api.

note that when you finally run the web frontend served from the same url 
where the rest api is running, you dont need this at all !

regards,
marcus.
msg7504 Author: [hidden] (rouilj) Date: 2022-05-04 14:40
Hi Marcus:

Hmm, I coded the rest interface with CORS headers. They return the
actual methods allowed for the end point. Any idea why it's not
working?

For example look in rest.py's dispatch() method for:

        # add access-control-allow-* to support CORS
        self.client.setHeader("Access-Control-Allow-Origin", "*")
        self.client.setHeader(
            "Access-Control-Allow-Headers",
            "Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override"
        )
        self.client.setHeader(
            "Allow",
            "OPTIONS, GET, POST, PUT, DELETE, PATCH"
        )
        self.client.setHeader(
            "Access-Control-Allow-Methods",
            "HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH"
        )

The Access-Control-Allow-Methods is modified for each route. For
example in rest.py find:

    @Routing.route("/data/<:class_name>", 'OPTIONS')
    @_data_decorator
    def options_collection(self, class_name, input):

which handles an OPTIONS request to e.g. /data/issue. At the end of
that method is an override:

        self.client.setHeader(
            "Access-Control-Allow-Methods",
            "OPTIONS, GET, POST"
        )
        return 204, ""

since PUT, PATCH and DELETE aren't valid for /data/issue.

I return these headers blind - I don't check the client's
Access-Control-Request-Method or Access-Control-Request-Headers.  But
that shouldn't matter if the headers are valid/allowed.

I did not implement Access-Control-Max-Age and
Access-Control-Allow-Credentials.  Max-Age should be optional. Reading
up on Access-Control-Allow-Credentials, it looks like I should be
returning that header set to true if credentials are supplied for the
options request. Supplying credentials is the normal use case.
 
Also I wildcard (*) Access-Control-Allow-Origin, so any origin is
accepted. Your code just mirrors the origin supplied by the client
which (should have) the same result.  Ah, I just reread the doc on it.
'*' only works when no credentials are supplied.

Can you try patching rest.py::dispatch() to set
Access-Control-Allow-Origin using the client header (as in your patch)
and add an:

  Access-Control-Allow-Credentials: true

header and see if CORS works without the patch you attached here.

We should not need "Vary: Origin" the response, since we aren't
filtering on the client's Origin header.

Also I am not sure if XMLRPC needs CORS. I am not even sure if xmlrpc
will respond to an OPTIONS query. IIRC the xmlrpc endpoint uses only
POST requests. If we implement this, it might be enough to add support
for OPTIONS on that end point, reply with the same headers as rest.py::dispatch()
but hard code the allow methods to POST.
 
> could make sense to also talk to the html frontend directly via CORS
> from within another webpage.

My initial thought is that iframe would be used for this which IIRC
doesn't use CORS. It has other restrictions in CSP etc. However IIRC
download/upload of files runs through the web endpoint so we might
need something there as well. What use cases do you have in mind?
Would Roundup supply HTML partials/fragments to be composed on the
client?
msg7509 Author: [hidden] (marcus.priesch) Date: 2022-05-05 09:47
Hello John,

the problem is that the preflight request comes without any
authorization (because it is asking the server what is allowed) and
therefore it always returns 403.

in client.py line 652 (with the patch applied) it reads:

if not self.db.security.hasPermission('Rest Access', self.userid):
     self.response_code = 403

user is "anonymous" here as we have no auth data in the preflight request.

thats what the patch in client.py (below "# handle preflight...")
addresses.

at this point the patch  allow everything that is supported down the way
(hopefully), assuming that at deeper levels somewhere things get
forbidden based on permissions and other rules.

the

	@Routing.route("/data/<:class_name>", 'OPTIONS')

section never gets hit at this point, because it only would if someone
has "Rest Access" permission. which anonymous dont has.

but maybe it should for that reason ? maybe that would be too open ?
maybe anonymous should just have enough permission to reach the code
above to send correct cors headers? - which probably would be better
than sending one answer for all ? ... but i am not that deep in the
permission thing if this is possible :(

acc. to max-age i found out that Access-Control-Max-Age indeed is a time
saver when you access same endpoint within the time range as the OPTIONS
request then gets not sent by the browser. maybe this should be
configurable for the user ?

> My initial thought is that iframe would be used for this which IIRC
> doesn't use CORS. It has other restrictions in CSP etc. However IIRC
> download/upload of files runs through the web endpoint so we might
> need something there as well. What use cases do you have in mind?
> Would Roundup supply HTML partials/fragments to be composed on the
> client?

i also think that iframes dont use cors, but iframes are ugly anyway ;)

i dont have anything in mind, but i can imagine someone building a
frontend requesting html fragments (maybe running through tal for
i18n ;)) or something else ...

i am rather thinking about where to put the code to address all possible
use cases at once ...

when you say xmlrpc is working over post - hmmm, i dont think it would
be needed because most likely there will be no browser talking to it ...
but can be, if someone is wierd enough to build a webpage using xmlrpc
instead of rest ;)

in the end it's all just plain http requests ... so i would vote to have
the CORS thing for all http requests.

regards,
marcus.
msg7510 Author: [hidden] (rouilj) Date: 2022-05-05 13:54
Hi Marcus:

In message <52323af0-99c1-043d-4573-c954f914f52c@priesch.co.at>,
Marcus Priesch writes:
>the problem is that the preflight request comes without any
>authorization (because it is asking the server what is allowed) and
>therefore it always returns 403.

Hmm, have you tried making a credentialed request?

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#requests_with_credentials
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included

So rather than a simple fetch to /rest/issue/23 you do a credentialed
fetch there.

I would think you would have to do that anyway as /rest/issue/23 may
not be readable to anonymous (you can control permission on a class
(e.g. can't access any user item), an item (you can access only your
user item), or property (you can't access the Role property of your
user item). All three of these are exposed as endpoints in the REST
interface.

>in client.py line 652 (with the patch applied) it reads:
>
>if not self.db.security.hasPermission('Rest Access', self.userid):
>     self.response_code = 403
>
>user is "anonymous" here as we have no auth data in the preflight request.

Does it work with a non-credentialed preflight if anonymous has 'Rest
Access' permission? (See below for how to enable.)

>thats what the patch in client.py (below "# handle preflight...")
>addresses.
>
>at this point the patch allow everything that is supported down the way
>(hopefully), assuming that at deeper levels somewhere things get
>forbidden based on permissions and other rules.

You are correct that a delete against /rest/issue would be rejected
regardless of what CORS reports. However if somebody codes an
interface that trusts what CORS reports it could get ugly by showing
users things that will not work.

>	@Routing.route("/data/<:class_name>", 'OPTIONS')
>
>section never gets hit at this point, because it only would if someone
>has "Rest Access" permission. which anonymous dont has.

We could add a "CORS Access" right and check to see if it's an OPTIONS
with the proper header subset (Allow-*). So your patch would not
perform the response but would permit the request to bypass the 403. (1)

However if anonymous is not allowed Rest Access, you will have to pass
credentials when performing the actual POST/DELETE/PUT/GET call. So
the credentials for the rest end point should be passed for CORS as
well.

>but maybe it should for that reason ? maybe that would be too open ?

Currently the Anonymous user has "Web Access" rights in almost all
cases otherwise you can't log in. They may still be unable to see any
data (due to the permissions model). For anybody implementing a REST
based front end I think anonymous should have REST access.

Consider the roundup-tracker. Anonymous is allowed read only access to
issues. Implementing the same via rest would have to allow anonymous
"Rest Access" permissions.

Simply add:

  db.security.addPermissionToRole('Anonymous', 'Rest Access')

at the end of schema.py and restart the server.

>maybe anonymous should just have enough permission to reach the code
>above to send correct cors headers? - which probably would be better
>than sending one answer for all ? ... but i am not that deep in the
>permission thing if this is possible :(

We would need to add a patch as in (1).

>acc. to max-age i found out that Access-Control-Max-Age indeed is a time
>saver when you access same endpoint within the time range as the OPTIONS
>request then gets not sent by the browser. maybe this should be
>configurable for the user ?

For right now, hard code the lifetime to say a week or so. I think
this is ok since:

  1. the access data is static. The only way the permissions can
     change is by changing the schema and restarting the server.

  2. if the cached access settings are invalid (become stale) the client
     will make a request that returns a 403 or other error. This isn't
     great but if it only occurs after permissions are changed it should
     not occur often.

So there is no security or data integrity issues with caching the
Access results too long. If we need to make that configurable in
config.ini or extract it into something that can be overridden using
interfaces.py we can discuss it when we see the need.

>[...] What use cases do you have in mind?
>> Would Roundup supply HTML partials/fragments to be composed on the
>> client?
> [...]
>i dont have anything in mind, but i can imagine someone building a
>frontend requesting html fragments (maybe running through tal for
>i18n ;)) or something else ...

Ok, so my scenario isn't totally crazy.

>i am rather thinking about where to put the code to address all possible
>use cases at once ...

I am not sure that is possible to do accurately.

  1. for rest we need to parse the route to determine if it's valid
     endpoint (rest/isue for example should fail)
  2. for rest we should to return only supported methods (e.g. not
     include DELETE for collection endpoints)
  3. for xmlrpc it only supports POST
  4. for http, the verb I think is only GET or POST. The actual verb
     is defined in the @action component of the query string.
     It's not even determined by a path component in the
     URL. So create is @action=new and retire/delete is @action=retire.

>when you say xmlrpc is working over post - hmmm, i dont think it would
>be needed because most likely there will be no browser talking to it ...
>but can be, if someone is wierd enough to build a webpage using xmlrpc
>instead of rest ;)

True, today you would probably use the rest endpoint and not XMLRPC,
but in AJAX, the X is XML. (FYI the rest endpoint can return XML by
adding a python module and setting the client content headers.)

>in the end it's all just plain http requests ... so i would vote to have
>the CORS thing for all http requests.

Well CORS is used to determine what verbs are valid for an endpoint.
XMLRPC and HTTP/html use a very restricted set of verbs in general.
msg7511 Author: [hidden] (rouilj) Date: 2022-05-05 14:19
Hi Marcus:

A followup. It looks like preflight requests will never send
credentials regardless of the credential settings 8-(.

From:

 https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included

    CORS-preflight requests must never include credentials. The
    response to a preflight request must specify
    Access-Control-Allow-Credentials: true to indicate that the actual
    request can be made with credentials.

So for now I think allowing anonymous to have "Rest Access"
permissions and establishing a new "CORS Access" permission that
permits OPTIONS with CORS headers is the way to go.

However this opens a can of worms if CORS is applied to all three
services:

  1 rest
  2 xmlrpc
  3 html

Do we need "CORS-rest Access", "CORS-xmlrpc Access" .... permissions
to go along with the "Rest Access", "Web Access", "XMLRPC Access"?

Also should "Rest Access" imply "CORS-Rest Access" so we don't have to
add multiple permissions to a role.

At the moment, I am leaning toward just a "CORS Access" that will work
for any service (rest, xmlrpc, html). It is checked only if the user
doesn't have access to the service (e.g via "Rest Access", "Web
Access"). (This is similar to how roundup already handles search. It
checks for view access to the searched class/property and if not found
looks for search access permissions to the class/property.)

While it is possible to probe for endpoints using a CORS request, I
don't think this a major problem. At the moment the http endpoint
treats an OPTIONS request like a GET request and returns an html page.
The XMLRPC endpoint just hangs waiting for a document/body which will
never come since there is no body/payload in options requests.

Thoughts?
msg7512 Author: [hidden] (marcus.priesch) Date: 2022-05-06 10:37
Hi John,

> A followup. It looks like preflight requests will never send 
> credentials regardless of the credential settings 8-(.
[...]
> Thoughts?

cors-preflight is a different story than the actual cors thing.

i am not really sure if we have common sense here, therefore let me
briefly sum it up:

1) when you issue request to a different url from where the page
actually was loaded in javascript (== CORS), then

2) the browser automatically sends a cors-preflight request right before
your actual request to query the server what he is allowed to do.

this request has no auth info as it's just there to query the server how
to continue, if cors actually is allowed, what headers are allowed, is
sending credentials allowed or not, etc.

3) the answer includes allowed headers, if sending credentials is
allowed, what methods are allowed, etc.

it is therefore an anonymous, public and general information.

this is covered by the first part of the patch.

of course we could limit access to certain "Origin"'s to limit who
acutally is allowed to do cors at all.

4) with this information the browser then crafts the actual cors request
and sends it to the server (including auth cookies - if allowed, uses
only allowed headers, etc).

5) the answer then again needs to have a couple of headers set to inform
the browser that it is still allowed to continue (that covers the second
part of the patch)

therefore you actually have two requests in this case.

so, the question is, where to put the logic and what actually gets sent
in 3)?

letting this all come down through the permission system to the actual
OPTIONS method you mentioned in an earlier post, maybe is not that
useful (and a bit of an overkill) as you basically would have the
same answer for almost any endpoint.

except the allowed methods for that endpoint.

but i think that e.g. when 3) returns that PATCH is allowed and the
browser then issue a PATCH on an item where PATCH actually is *not*
allowed, the browser gets back the error anyway ...

i think we can live with this as it would make things easier.

so for me it would be enough to have a config option "ALLOW_CORS" which
enables/disables the answer to the preflight request (first part of the
patch - 3) form above)

optionally maybe also configurable / default:

   Access-Control-Max-Age / 86400
   Access-Control-Allow-Methods / OPTIONS, GET, POST, PUT, DELETE, PATCH
   Access-Control-Allow-Credentials / true

and maybe some sort of configuring what Origins are allowed.

"*" meaning that the requests Origin header just gets copied (as in the
patch) and some kind of check if it's not "*", maybe regex on the Origin
header ?

the second part of the patch needs to be applied anyway as the browser
dont accept Access-Control-Allow-Credentials: true without
Access-Control-Allow-Origin being set to the Origin of the request (as
you already found out).

And you actually need credentials when you want to do something user
specific with the rest api (as in my case).

... just my thoughts ...

regards,
marcus.
msg7513 Author: [hidden] (rouilj) Date: 2022-05-08 23:55
Hi Marcus:

In message <42c5af33-fdde-e0c4-96f7-b4908a4bd174@priesch.co.at>,
Marcus Priesch writes:
>> A followup. It looks like preflight requests will never send 
>> credentials regardless of the credential settings 8-(.
>[...]
>> Thoughts?
>
>cors-preflight is a different story than the actual cors thing.
>
>i am not really sure if we have common sense here,

I agree. Thanks for the summary.

>briefly sum it up:

>1) when you issue request to a different url from where the page
>actually was loaded in javascript (== CORS), then

Just to clarify it's not a different url it's a different origin right?
So serving up data from:

   https://example.com/roundup

would be ok from:

   https://example.com/myapp

but would not be ok from

   https://example.com:8080/myapp

since that is a different origin than example.com. (Also I think
changing to http from https might also change the origin??)

>2) the browser automatically sends a cors-preflight request right before
>your actual request to query the server what he is allowed to do.

For anything that is not a simple request. So a GET doesn't get
preflighted, right?

>this request has no auth info as it's just there to query the server how
>to continue, if cors actually is allowed, what headers are allowed, is
>sending credentials allowed or not, etc.
>
>3) the answer includes allowed headers, if sending credentials is
>allowed, what methods are allowed, etc.
>
>it is therefore an anonymous, public and general information.

Got it.

>this is covered by the first part of the patch.
>
>of course we could limit access to certain "Origin"'s to limit who
>acutally is allowed to do cors at all.

The REST interface can be programmed (see
https://www.roundup-tracker.org/docs/rest.html#programming-the-rest-api).
Using interfaces.py filtering the origin can be added on a per tracker
basis by overriding/replacing the existing handler in rest.py then
calling it from the override. It's a bit tricky and more fragile than
I would like but I think it can be done.

I agree it would be nice to have a way to set an Origin_Filter in
rest.py similar to how Client.Cache_Control can be set.

>4) with this information the browser then crafts the actual cors request
>and sends it to the server (including auth cookies - if allowed, uses
>only allowed headers, etc).
>
>5) the answer then again needs to have a couple of headers set to inform
>the browser that it is still allowed to continue (that covers the second
>part of the patch)
>
>therefore you actually have two requests in this case.
>
>so, the question is, where to put the logic and what actually gets sent
>in 3)?
>
>letting this all come down through the permission system to the actual
>OPTIONS method you mentioned in an earlier post, maybe is not that
>useful (and a bit of an overkill) as you basically would have the
>same answer for almost any endpoint.
>
>except the allowed methods for that endpoint.

Possibly. The question is what if my endpoint is invalid?  Do I return
a valid 204 preflight for:

  https:/example.com/roundup/rest/data/iss

where there is no iss class? I am not sure what the proper failure
mode is here, but it seems that we should return a 404 during
preflight.

By pushing it down into the rest library, you get sanity checking on
the URL. Plus as I mentioned above, a roundup-admin can add additional
checks on the preflight (e.g. origin, maybe the client IP) using an
existing documented mechanism (even if it is less streamlined than I
would like). Plus we supply the client/user with the correct methods.

>i think we can live with this as it would make things easier.

But easier for who? The roundup maintainers or the people using the
rest interface? I would prefer to make it easier for the users. At the
moment you and one other person are the only users I know of.  The
openapi_doc decorators were requested by him/her.

>so for me it would be enough to have a config option "ALLOW_CORS" which
>enables/disables the answer to the preflight request (first part of the
>patch - 3) form above)

Actually we don't really need it. REST is unusable from a third party
without anonymous CORS-preflight access. After your explanation I
can't see a reason to implement this. If a developer wants to
implement it they can by programming the OPTIONS endpoints in the REST
interface.

>optionally maybe also configurable / default:
>
>   Access-Control-Max-Age / 86400
>   Access-Control-Allow-Methods / OPTIONS, GET, POST, PUT, DELETE, PATCH
>   Access-Control-Allow-Credentials / true
>
>and maybe some sort of configuring what Origins are allowed.

All of these can be done by overriding the options endpoint as
documented above. If enough people clamor for this capability we can
expose it via a configuration item or via interfaces.py.

>the second part of the patch needs to be applied anyway as the browser
>dont accept Access-Control-Allow-Credentials: true without
>Access-Control-Allow-Origin being set to the Origin of the request (as
>you already found out).

Got it.

>... just my thoughts ...

Good thoughts.

I have attached my current patch that I claim should work. Can you
apply it and see if it works.

Also do you have a test case that I can use to test this? I think a
single HTML file with a couple of inputs (for username/password) along
with an input for a URL that has javascript that goes and pulls data
from the URL. Then I can load it in my browser and test things. I am
currently using curl and visually inspecting what is returned.

Thanks.
msg7516 Author: [hidden] (rouilj) Date: 2022-05-12 21:17
Hi Marcus:

I added documentation on CORS preflight and CORS to doc/rest.txt in
changeset:   6674:ff8845ca305e. Can you take  look at it and see where it needs 
correction/improvement.

Thanks.

-- rouilj
msg7517 Author: [hidden] (rouilj) Date: 2022-05-12 21:40
I finally got a coffee and read through: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

my original patch was broken. I tried to be to clever and not send headers that are needed
only by preflight but not CORS. I removed too many headers. The Fixed1 file should
be the one you try to use.
msg7518 Author: [hidden] (rouilj) Date: 2022-05-12 22:14
Two other notes:

I just added a Vary: Origin header to the rest responses since the existing OPTIONS
handlers can be wrapped to implement origin filtering and return different info based
on origin. 

CSRF Origin checks currently have to be turned off for CORS REST calls.
This check either needs to be:

  driven off a prefix table that can be set in config.ini or by interfaces.py.
  If the origin sent by the client is matched by a prefix:
      ['https://foo.bar/', 'https://example.com', 'http://example.com']
  in the table then the origin check passes. Right now the prefix is the tracker url.

  is re-implemented as a method on the client object that can be wrapped/overridden in
  interfaces.py.

I am leaning toward the prefix table from config.ini myself as I think it can be used to 
implement origin filtering for preflight requests in the core code.

An open question is what to return if an origin check fails in preflight or in cors.
* return some 400 value
* remove Access-Control-Allow-Origin header with no other change. This will fail a
  prefetch and prevent the data returned from being accessible to JavaScript IIUC.
msg7531 Author: [hidden] (marcus.priesch) Date: 2022-05-17 10:48
Hi John,

sorry, but i was off the last week ...

> I added documentation on CORS preflight and CORS to doc/rest.txt in 
> changeset:   6674:ff8845ca305e. Can you take  look at it and see
> where it needs correction/improvement.

looks good, maybe i would add some information that this is done by the
browser automatically and transparently for the user, if the preflight
request fails your ajax request also fails.
msg7532 Author: [hidden] (marcus.priesch) Date: 2022-05-17 10:55
> An open question is what to return if an origin check fails in preflight or in cors.
> * return some 400 value
> * remove Access-Control-Allow-Origin header with no other change. This will fail a
>    prefetch and prevent the data returned from being accessible to JavaScript IIUC.

i would vote for 403 (maybe 404 to not reveal too much)

and btw: how is the status now with the support for preflight requests ? 
what can i do ? - the patch you provided in an earlier post was rejected 
by you ... is there anything i can test or help ?

regards,
marcus.
msg7533 Author: [hidden] (rouilj) Date: 2022-05-17 15:01
Hi Marcus:

No worries about time off. It's a good idea in general.  I'll update
the docs.

In message <71471f9c-d38e-35a2-0739-835fc127475f@priesch.co.at>,
Marcus Priesch writes:
>> An open question is what to return if an origin check fails in
>> preflight or in cors.
>> * return some 400 value
>> * remove Access-Control-Allow-Origin header with no other change.
>>   This will fail a prefetch and prevent the data returned from being
>>   accessible to JavaScript IIUC.
>
>i would vote for 403 (maybe 404 to not reveal too much)

404 indicates the URL endpoint is bad. The URL is fine it's just the
request that is bad. Some proxy's/caches invalidate data on a 404 (GET
method). I am not sure if a 404 with an OPTIONS method could be cached
or cause side effects.

Per

https://stackoverflow.com/questions/33739107/how-to-handle-an-invalid-cors-preflight-request

tomcat returns a 403. But the MDN docs say this about 403:

  ...Unlike 401 Unauthorized, the client's identity is known to the server. 

We aren't sending credentials/identity for a preflight, so I don't
think 403 is right. 403 is correct if a CORS /rest request occurred
for a user without "Rest Access" permissions. 401 might be valid even
though CORS isn't an authentication/authorization mechanism.

https://stackoverflow.com/questions/14015118/what-is-the-expected-response-to-an-invalid-cors-request

notes the result code is not specified and recommends 403.

https://stackoverflow.com/questions/69427953/cors-cross-origin-resource-sharing-origin-validation-failure

indicates returning 200 without the Access-Control-Allow-Origin header.

>and btw: how is the status now with the support for preflight requests ? 
>what can i do ? - the patch you provided in an earlier post was rejected 
>by you ... is there anything i can test or help ?

When I rejected my previous patch, I added a new patch. Try:

  https://issues.roundup-tracker.org/file1784/Fixed1_Allow_CORS_Preflight_through_rest_handlers.patch

and see if that works for you.

This patch does not add "Origin" to the Vary header. For your
testing purposes it doesn't matter. In real life, the
Access-Control-Allow-Origin could be different depending on the
origin.

Also can you confirm that your are using your patch with
csrf_enforce_header_origin turned off (not yes or required). The
preflight request skips the CSRF check, but with origin checks turned
on it should reject the actual CORS request.

Looks like I will need to allow the user to provide a list of valid
origins and add the tracker origin to the list by default. If * is in
the origin list, I pass any origin. I should do this for xmlrpc and
rest. For the html interface, only the tracker will be valid.

Also can you post the headers from one of your CORS requests? I need
to verify that the checks for Host/X-Forwarded-Host and Referer will
pass. (I have a feeling Referer won't.)

Have a great week.
msg7537 Author: [hidden] (rouilj) Date: 2022-05-23 21:42
Marcus, any feedback on my last patch?

I have done a bit more work, and am working on tests. I can upload a new patch
if you want, but my changes don't affect the core functionality in the latest existing patch.

I plan on another release of Roundup on 13 July, so I will be making a beta in a
week or two. Ideally these changes would be in the beta so we can release them
as part of 2.2.0.
msg7557 Author: [hidden] (rouilj) Date: 2022-06-01 14:38
Updating to newest patch with test case changes. Marcus if you haven't tested already with the 
first patch, use this one. If you have tested with the first patch let me know if
you need any fixes.

The one change here is a 400 return status if the valid origin check fails. Since 401/403 are
authentication/authorization issues they didn't seem right for an origin check failure.
(Arguably, the origin check failure is an authorization issue for the request not the user
making the request but 403 seems user based. But this may be my misunderstanding.)
msg7559 Author: [hidden] (rouilj) Date: 2022-06-07 12:48
Interesting that Marcus' email doesn't show up in the web interface.
I did receive it. Ralf did you get it as well?

Forwarding the body to see if this causes an error of some sort.

Also Marcus sorry you were out of sick leave. Hope you are feeling
better. Thanks for the confirmation that this works. Did you use my
second patch (Fixed2) or the original(Fixed1)?

   ====
Content-Transfer-Encoding: quoted-printable
Subject: [issue2551203] Add support for CORS preflight request
From: Marcus Priesch <issues@roundup-tracker.org>
Date: Tue, 07 Jun 2022 09:56:18 +0000

Marcus Priesch added the comment:

Hi John,

sorry for not answering for that long, but i was off for sick-leave :(

Thanks for the patch, i have tested it and it works !

unsetting allowed_api_origins leads to a 400, setting it to * or
http://localhost:8081 (where the frontend server is running) works :)

so i think for this we can apply this patch and go ahead ;)

btw: as per your request, here are the headers from the preflight=20
request as seen from devtools:

request:

OPTIONS=20
/t/rest/data/time_activity?@sort=3Dname&@verbose=3D0&@fields=3Dname,id,desc=
ription,travel&is_valid=3D1=20
HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=3D0.9,de;q=3D0.8
Access-Control-Request-Headers: content-type,x-requested-with
Access-Control-Request-Method: GET
Cache-Control: no-cache
Connection: keep-alive
Host: localhost:8080
Origin: http://localhost:8081
Pragma: no-cache
Referer: http://localhost:8081/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML,=20
like Gecko) Chrome/101.0.4951.64 Safari/537.36
=EF=BF=BC
answer:

HTTP/1.1 204 No Content
Server: BaseHTTP/0.3 Python/2.7.18
Date: Tue, 07 Jun 2022 09:49:24 GMT
Access-Control-Allow-Methods: OPTIONS, GET, POST
Access-Control-Max-Age: 86400
Vary: Origin, Origin
Allow: OPTIONS, GET, POST
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:8081
Access-Control-Allow-Headers: Content-Type, Authorization,=20
X-Requested-With, X-HTTP-Method-Override

thanks for the nice work and good support !

regards,
marcus.

------- End of Forwarded Message
msg7560 Author: [hidden] (rouilj) Date: 2022-06-07 12:54
Hmm my last message made it onto the ticket as msg7559, but msg7558
doesn't exist. msg7557 does exist. I'll try reaching out to some of
the PSF folks (ezio M). Never seen a failure like this before.

-- rouilj
msg7561 Author: [hidden] (marcus.priesch) Date: 2022-06-07 13:21
> Interesting that Marcus' email doesn't show up in the web interface.
> I did receive it. Ralf did you get it as well?

i got a submission failure email:

There was a problem with the message you sent:
    'ascii' codec can't decode byte 0xef in position 1451: ordinal not 
in range(128)

there were some non-ascii characters in the body of the mail left over 
from copying the text from the browsers devtools page.

interesting that there was an failure submission, nosy mail, but the 
message was not stored in the db ... looks like roundup bug ;)

i have the original mail and the failure submission still at hand - if 
you need if for some tests ...

> Also Marcus sorry you were out of sick leave. Hope you are feeling
> better. Thanks for the confirmation that this works. Did you use my
> second patch (Fixed2) or the original(Fixed1)?

i used the Fixed2 !

regards,
marcus.
msg7562 Author: [hidden] (rouilj) Date: 2022-06-07 15:02
Hi Marcus:

In message <d80c754a-77da-9827-66f5-7e337eb6af1e@priesch.co.at>,
Marcus Priesch writes:
>Marcus Priesch added the comment:
>
>> Interesting that Marcus' email doesn't show up in the web interface.
>> I did receive it. Ralf did you get it as well?
>
>i got a submission failure email:
>
>There was a problem with the message you sent:
>    'ascii' codec can't decode byte 0xef in position 1451: ordinal not 
>in range(128)
>
>there were some non-ascii characters in the body of the mail

Probably smart quotes. A message is committed in 2 phases. The first
phase creates a msgXXXX.tmp file. Then renames it to msgXXXX.
The .tmp file for your message was still around.

Also we are running the 1.6.1 version of roundup for the tracker.  It
is unlikely to get upgraded unless we put together our own server and
handle mail ourselves etc.

>interesting that there was an failure submission, nosy mail, but the 
>message was not stored in the db ... looks like roundup bug ;)

IIRC that's an old bug:

 https://issues.roundup-tracker.org/issue2550740 
 https://issues.roundup-tracker.org/issue2550540

are probably related. They are from before I had taken over roundup
maintenance. I have no access to the original reporters anymore and
the patches don't make any sense to me. Also they are python 2
patches.  The changes to make Roundup handle python 2 and 3 mean the
patches have to reverse engineered. Internationalization, character
sets etc. are not my strong suit.

>i have the original mail and the failure submission still at hand - if 
>you need if for some tests ...

Can you open a new issue addnig the original mail and failure message
as attachments. I'll try running it through the gateway on my test
system to see if I can get it to break.

It may be too late to get it in 2.2.0 but.....

>> [...] Did you use my second patch (Fixed2) or the original(Fixed1)?
>
>i used the Fixed2 !

Good.

I committed it:

  changeset:   6693:9a1f5e496e6c

it passed CI for all pythons except 3.11-dev. However it's failing
ther with an disk i/o error in the sqlite tests. So I claim it's a
platform and not a roundup issue.

I think roundup needs to get off travis-ci.

codecov is being a pain but it looks like I have 100% coverage for
changed lines so there's that.
History
Date User Action Args
2022-06-07 22:08:35rouiljsettitle: Add support for CORS preflight request (fwd) -> Add support for CORS preflight request
2022-06-07 18:18:54rouiljsetpriority: normal
status: open -> fixed
resolution: fixed
2022-06-07 15:02:24rouiljsetmessages: + msg7562
2022-06-07 13:21:41marcus.prieschsetmessages: + msg7561
2022-06-07 12:54:45rouiljsetmessages: + msg7560
2022-06-07 12:48:30rouiljsetmessages: + msg7559
title: Add support for CORS preflight request -> Add support for CORS preflight request (fwd)
2022-06-01 14:38:31rouiljsetfiles: + Fixed2_Allow_CORS_Preflight_through_rest_handlers.patch
messages: + msg7557
2022-05-23 21:42:26rouiljsetassignee: rouilj
messages: + msg7537
components: + Web interface, API
2022-05-17 15:01:15rouiljsetmessages: + msg7533
2022-05-17 10:55:25marcus.prieschsetmessages: + msg7532
2022-05-17 10:48:03marcus.prieschsetmessages: + msg7531
2022-05-12 22:16:23rouiljsetstatus: new -> open
2022-05-12 22:14:41rouiljsetmessages: + msg7518
2022-05-12 21:40:59rouiljsetfiles: + Fixed1_Allow_CORS_Preflight_through_rest_handlers.patch
messages: + msg7517
2022-05-12 21:17:09rouiljsetmessages: + msg7516
2022-05-08 23:55:58rouiljsetfiles: + Allow_CORS_Preflight_through_rest_handlers.patch
messages: + msg7513
2022-05-06 10:37:20marcus.prieschsetmessages: + msg7512
2022-05-05 14:19:32rouiljsetmessages: + msg7511
2022-05-05 13:54:47rouiljsetmessages: + msg7510
2022-05-05 09:47:47marcus.prieschsetmessages: + msg7509
2022-05-04 14:40:28rouiljsetmessages: + msg7504
2022-05-04 06:21:39marcus.prieschcreate