Closed Bug 1738251 Opened 3 years ago Closed 1 year ago

POST with basic Auth to CORS is blocked with no explanation

Categories

(Core :: DOM: Networking, defect, P3)

Firefox 93
defect

Tracking

()

RESOLVED FIXED
110 Branch
Tracking Status
firefox110 --- fixed

People

(Reporter: utopiabound, Assigned: valentin)

References

(Blocks 3 open bugs)

Details

(Whiteboard: [necko-triaged])

Attachments

(1 file)

User Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:93.0) Gecko/20100101 Firefox/93.0

Steps to reproduce:

Use Case: Code flow authentication via Oauth2 to reddit.com via javascript
Tracking Protection is disabled in the browser.
POST to a remote site (https://www.reddit.com/api/v1/access_token) with "Authorization: Basic "+btoa(ID+":") set in the header.

Actual results:

DevTools shows "Blocked" in "Transferred" for the POST, there is not OPTIONS prior as one would expect for a CORS lookup.
There is no explanation for why it was blocked.

Expected results:

The POST should have send the data to the server and received json data back. This works in Chrome and Safari.

The same misbehavior is observed in Firefox for iOS, but Safari on the same device works correctly.

The Bugbug bot thinks this bug should belong to the 'Core::Privacy: Anti-Tracking' component, and is moving the bug to that component. Please revert this change in case you think the bot is wrong.

Component: Untriaged → Privacy: Anti-Tracking
Product: Firefox → Core
Component: Privacy: Anti-Tracking → DOM: Security

Correction: Firefox of iOS does not exhibit this behavior, it functions like Safari, and loads the endpoint correctly (working in Firefox Daylight 39.0).

All browsers on iOS are webkit (safari) underneath. Apple rules do not allow any other "web engine", only different browser features built on top.

POST how? an actual form? XMLHttpRequest? fetch()? what options are you using in the call? Do you have a site or testcase to demonstrate this? Even just a snippet of the relevant code?

Component: DOM: Security → DOM: Networking

https://redditp.utopiabound.net - then login to reddit: little person in bottom left > little person icon. This runs through the "Code flow" OAuth2 token retrieval (https://github.com/reddit-archive/reddit/wiki/OAuth2#token-retrieval-code-flow) which fails on "Retrieving the access token".

The actual access is:

var jsonUrl = 'https://www.reddit.com/api/v1/access_token';
var data = {
    grant_type: 'authorization_code',
    code: code,
    redirect_uri: rp.redirect
};
$.ajax({
            url: jsonUrl,
            method: 'POST',
            data: data,
            dataType: 'json',
            headers: {
                "Authorization": "Basic " + btoa(rp.api_key.reddit + ":")
            },
            username: rp.api_key.reddit,
            password: '',
            success: handleData,
            error: handleError,
            timeout: rp.settings.ajaxTimeout,
            crossDomain: true
});

I can reproduce this, thanks for filing. I think this is something we probably want to figure out soon.

Christoph, do you know off the top of your head if the username prefix in the URL (https://foo@bar.com) would cause us to misdiagnose the request as violating a cross-origin policy?

Tentatively marking as P2 until we understand this better.

Severity: -- → S2
Flags: needinfo?(ckerschb)
Priority: -- → P2
Whiteboard: [necko-triaged]

Per Fetch a 401 response is required before another request is made that will include the Authorization header based on the credentials included in the URL. However, if you get such a 401 response it will not be handled automatically for CORS requests.

What might happen is that other browsers unconditionally include the Authorization header based on the credentials included in the URL. That would go against the specification, but test coverage is not great I believe.

(I also seem to remember that including credentials was not allowed at all for CORS requests, but it seems that only redirects end up breaking if the Location header value contains them. It still breaks because of the 401 response dance, but I thought there was something more explicit that I cannot find now.)

(In reply to Nihanth Subramanya [:nhnt11] from comment #5)

Christoph, do you know off the top of your head if the username prefix in the URL (https://foo@bar.com) would cause us to misdiagnose the request as violating a cross-origin policy?

The short answer is No, it shouldn't, we should only compare scheme, host, port for performing same-origin checks.

The longer answer and probably helpful for debugging:

Flags: needinfo?(ckerschb)

Christoph, should your team take this?

Flags: needinfo?(ckerschb)

We will take care of this.

Flags: needinfo?(ckerschb)

Any ETA for this?

This is still broken in 99.0.1

I've poked this a bit more and found that I have a workaround for this issue, if I replace the above .ajax call with this:

        $.ajax({                                                                                                                                                                                                                                                                    
            url: jsonUrl,                                                                                                                                                                                                                                                           
            method: 'POST',                                                                                                                                                                                                                                                         
            data: data,                                                                                                                                                                                                                                                             
            dataType: 'json',                                                                                                                                                                                                                                                       
            headers: {                                                                                                                                                                                                                                                              
                "Authorization": "Basic " + btoa(rp.api_key.reddit + ":")                                                                                                                                                                                                           
            },                                                                                                                                                                                                                                                                      
            success: handleData,                                                                                                                                                                                                                                                    
            error: handleError,                                                                                                                                                                                                                                                     
            timeout: rp.settings.ajaxTimeout,                                                                                                                                                                                                                                       
            crossDomain: true                                                                                                                                                                                                                                                       
        });                                                                                                                                                                                                                                                                         

It will do OPTIONS to jsonUrl, and then a POST to jsonUrl with Authorization: header and get back correct keys and run happily.

With the username: and password: set (with or without the manual headers: settings) in the above example, it fails to do the OPTIONS and just reports "blocked" for the POST operation.

$.ajax does XHR, but this may also affect to fetch.

Blocks: xhr, fetch
Severity: S2 → S3
Priority: P2 → P3

I just happened to see this bug and the code, hope that this helps:

It seems that this part of nsCORSListenerProxy::UpdateChannel may potentially be causing this bug:

  nsCString userpass;
  uri->GetUserPass(userpass);
  NS_ENSURE_TRUE(userpass.IsEmpty(), NS_ERROR_DOM_BAD_URI);

If I read the code correctly, then the presence of a username/password component in the requested URL immediately causes a CORS failure without extra logging (no LogBlockedRequest call).

Thank you for pointing us to that code, Rob.
I tried disabling that check in a try push but it starts failing a bunch of WPT.

Judging by the failing tests we ought to block cross-origin redirects to a URL with credentials, but we also apply this to any cross-origin URI.

All of the WPT are checking that cross origin redirects to URLs with
user:password are blocked, but Firefox was blocking any CORS request
to a URL with user:password.

Related WPT:
fetch/api/cors/cors-redirect-credentials.any.js
fetch/security/redirect-to-url-with-credentials.https.html
cors/redirect-userinfo.htm
service-workers/service-worker/fetch-event-redirect.https.html
xhr/access-control-and-redirects-async.any.html

Assignee: nobody → valentin.gosu
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true

(In reply to Anne (:annevk) from comment #6)

(I also seem to remember that including credentials was not allowed at all for CORS requests, but it seems that only redirects end up breaking if the Location header value contains them. It still breaks because of the 401 response dance, but I thought there was something more explicit that I cannot find now.)

Anne, I'm unable to locate the part of the spec that indicates CORS redirects with user:password should fail. Do you happen to know where that is?

Flags: needinfo?(annevk)
Pushed by valentin.gosu@gmail.com:
https://hg.mozilla.org/integration/autoland/rev/ebc1b62a0019
CORS requests to URL with userpassword should only fail for redirects r=necko-reviewers,kershaw

Backed out for causing mochitest failures on test_CrossSiteXHR.html.

Push with failures

Failure log

Backout link

[task 2023-01-11T16:19:56.355Z] 16:19:56     INFO - TEST-PASS | dom/security/test/cors/test_CrossSiteXHR.html | wrong responseText in test for {"pass":1,"method":"GET","noAllowPreflight":1} 
[task 2023-01-11T16:19:56.355Z] 16:19:56     INFO - Buffered messages finished
[task 2023-01-11T16:19:56.356Z] 16:19:56     INFO - TEST-UNEXPECTED-FAIL | dom/security/test/cors/test_CrossSiteXHR.html | should have failed in test for {"pass":0,"method":"GET","noAllowPreflight":1,"username":"user"} - got false, expected true
[task 2023-01-11T16:19:56.356Z] 16:19:56     INFO -     SimpleTest.is@SimpleTest/SimpleTest.js:487:14
[task 2023-01-11T16:19:56.357Z] 16:19:56     INFO -     runTest@dom/security/test/cors/test_CrossSiteXHR.html:759:9
[task 2023-01-11T16:19:56.357Z] 16:19:56     INFO -     initTestCallback/<@dom/security/test/cors/test_CrossSiteXHR.html:39:9
[task 2023-01-11T16:19:56.357Z] 16:19:56     INFO - Not taking screenshot here: see the one that was previously logged
[task 2023-01-11T16:19:56.359Z] 16:19:56     INFO - TEST-UNEXPECTED-FAIL | dom/security/test/cors/test_CrossSiteXHR.html | wrong status in test for {"pass":0,"method":"GET","noAllowPreflight":1,"username":"user"} - got 200, expected +0
[task 2023-01-11T16:19:56.359Z] 16:19:56     INFO -     SimpleTest.is@SimpleTest/SimpleTest.js:487:14
[task 2023-01-11T16:19:56.359Z] 16:19:56     INFO -     runTest@dom/security/test/cors/test_CrossSiteXHR.html:761:9
[task 2023-01-11T16:19:56.359Z] 16:19:56     INFO -     initTestCallback/<@dom/security/test/cors/test_CrossSiteXHR.html:39:9
[task 2023-01-11T16:19:56.360Z] 16:19:56     INFO - Not taking screenshot here: see the one that was previously logged
[task 2023-01-11T16:19:56.361Z] 16:19:56     INFO - TEST-UNEXPECTED-FAIL | dom/security/test/cors/test_CrossSiteXHR.html | wrong status text for {"pass":0,"method":"GET","noAllowPreflight":1,"username":"user"} - got "OK", expected ""
[task 2023-01-11T16:19:56.361Z] 16:19:56     INFO -     SimpleTest.is@SimpleTest/SimpleTest.js:487:14
[task 2023-01-11T16:19:56.361Z] 16:19:56     INFO -     runTest@dom/security/test/cors/test_CrossSiteXHR.html:762:9
[task 2023-01-11T16:19:56.362Z] 16:19:56     INFO -     initTestCallback/<@dom/security/test/cors/test_CrossSiteXHR.html:39:9
[task 2023-01-11T16:19:56.363Z] 16:19:56     INFO - Not taking screenshot here: see the one that was previously logged
[task 2023-01-11T16:19:56.364Z] 16:19:56     INFO - TEST-UNEXPECTED-FAIL | dom/security/test/cors/test_CrossSiteXHR.html | wrong responseXML in test for {"pass":0,"method":"GET","noAllowPreflight":1,"username":"user"} - got "<res>hello pass</res>", expected null
[task 2023-01-11T16:19:56.365Z] 16:19:56     INFO -     SimpleTest.is@SimpleTest/SimpleTest.js:487:14
<...>
Flags: needinfo?(valentin.gosu)
Flags: needinfo?(valentin.gosu)
Pushed by valentin.gosu@gmail.com:
https://hg.mozilla.org/integration/autoland/rev/faaa23eb1ce7
CORS requests to URL with userpassword should only fail for redirects r=necko-reviewers,kershaw
Status: ASSIGNED → RESOLVED
Closed: 1 year ago
Resolution: --- → FIXED
Target Milestone: --- → 110 Branch
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: