10

Update 2 (A complete set of logs)

From Client's Perspective

Request headers:

POST /dev/micro_server.php HTTP/1.1 Host: production-server.com
Connection: keep-alive
Content-Length: 86
Pragma: no-cache
Cache-Control: no-cache
Accept: text/html, /; q=0.01
Origin: https://debug.dev
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 OPR/58.0.3135.90
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: https://debug.dev/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: debugger_session=iq4tbdk374mtvodsd3edcf2jq5

Response headers:

HTTP/1.1 200 OK
Server: nginx/1.4.6 (Ubuntu)
Date: Tue, 12 Mar 2019 12:01:27 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/5.5.9-1ubuntu4.17
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Origin: https://production-server.com
Access-Control-Allow-Credentials: true
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Encoding: gzip

Console Error:

Access to XMLHttpRequest at 'https://production-server.com/dev/micro_server.php' from origin 'https://debug.dev' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'https://production-server.com' that is not equal to the supplied origin.

Console Warning:

Cross-Origin Read Blocking (CORB) blocked cross-origin response https://daikai.no/dev/micro_server.php with MIME type text/html. See https://www.chromestatus.com/feature/5629709824032768 for more details.

From Server's Perspective

This is what the server says it has received and sent (check the code that does the logging in update 1):

Array
(
    [req] => Array
        (
            ...
            [HTTP_ORIGIN] => https://debug.dev
            ...
        )

    [rsp] => Array
        (
            [0] => X-Powered-By: PHP/5.5.9-1ubuntu4.17
            [1] => Access-Control-Allow-Origin: https://debug.dev
            [2] => Access-Control-Allow-Methods: GET, POST, OPTIONS
            [3] => Access-Control-Allow-Credentials: true
        )

)

Update

I have added some logging on the server and the script now begins with these lines:

# allow access from other domains
    header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
    header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
    header("Access-Control-Allow-Credentials: true");

$all = [
    'req' => $_SERVER,
    'rsp' => headers_list()
];

$s = print_r($all, true);
$p = '/var/www/path/to/file_' . uniqid() . '.txt';
file_put_contents($p, $s);

With this I can confirm that the request arrives on the server with the correct Origin, AND the server sends back the correct CORS headers. Yet, the Access-Control-Allow-Origin in the developer console is wrong and the request is blocked.

Here is a stripped down log obtained with the code above:

Array
(
    [req] => Array
        (
            ...
            [HTTP_ORIGIN] => https://debug.dev
            ...
        )

    [rsp] => Array
        (
            [0] => X-Powered-By: PHP/5.5.9-1ubuntu4.17
            [1] => Access-Control-Allow-Origin: https://debug.dev
            [2] => Access-Control-Allow-Methods: GET, POST, OPTIONS
            [3] => Access-Control-Allow-Credentials: true
        )

)

Question

How and why does the Access-Control-Allow-Origin is changed to https://production.com when the actual header received is Access-Control-Allow-Origin: https://debug.dev?


(Original Post)

Background

I have a web-based debug tool that I have installed on my local development machine. With an entry in my /etc/hosts I have assigned to it the domain debug.dev. I have also added a local CA authority and have successfully created a SSL certificate for the domain name so now I can open https://debug.dev/ in my browser and the debug tool opens normally.

This tool is supposed to work with staging and production servers. So it needs to send AJAX requests to other domains. I have full control over those servers and I am sending back CORS headers from those servers like so:

header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
header("Access-Control-Allow-Credentials: true");

The Issue

Now I am facing a baffling situation in which when I send an AJAX request to the production server I get back Wrong CORS headers with the SERVER's domain like this:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://production-server.com

But if I right click and use Open in new tab the CORS headers are what they ought to be; i.e.

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://debug.dev

The only differences between the requests as far as I can see is that the first one is sent as an AJAX POST request and thus sends a HTTP_X_REQUESTED_WITH header whereas the second request is sent as an ordinary GET request. How could this result in a different CORS header be returned by the server?

Majid Fouladpour
  • 29,356
  • 21
  • 76
  • 127
  • 2
    Provide also full output from browser console. (enable also `info` messages) Also show all send and received headers for OPTIONS request. – bato3 Mar 10 '19 at 09:46
  • 2
    most likely insufficient information is provided to answer this question... eg. the page being served by the wrong server (unplug the cable and it might be unreachable). a quick and dirty fix would be to return both host-namens in the same header.... or just step into it with `xdebug`. – Martin Zeitler Mar 12 '19 at 05:56
  • 1
    I got tied up and didn't get to answer sooner. @Soleil, no they are on different machines. debug.dev is on the local machine (127.0.0.1) and production-server.com is a Digitalocean droplet. – Majid Fouladpour Mar 12 '19 at 09:35
  • 1
    @MartinZeitler all evidence, including logging on the production-server.com indicates the server *is* sending the correct headers. On the receiving end though we get a different value for the Origin header. I will try to make it clearer in the question. – Majid Fouladpour Mar 12 '19 at 09:41
  • 2
    @MajidFouladpour the logging does not necessarily confirm that the request hits this instance (unless logging timestamp & IP). while setting a break-point there certainly would. how should the client rewrite the header's value... without even knowing about the other host-name? searching the whole code-base for `header` & hard-coded host-names might also be something I'd try, because the header's value could theoretically be overwritten. – Martin Zeitler Mar 12 '19 at 09:50
  • Open the Network pane in browser devtools and examine all the requests and responses shown there — including the response headers — and copy and paste response headers into the question. – sideshowbarker Mar 12 '19 at 11:35
  • Do you see the same behaviour in multiple browsers? Browser extensions can modify the response before it hits the browser itself. – Alexander Mar 13 '19 at 09:35
  • @Alexander yes that is happening in all and every browser I have on my dev box, that includes Firefox Developer Edition, Google Chrome and Opera; and in Opera I have no extension installed at all, so I guess disabling extensions in other browsers would not be a way out of the rabbit hole. – Majid Fouladpour Mar 13 '19 at 11:32
  • @MartinZeitler even though this is the production server, the url is not something discoverable without one knowing it exists. It therefore gets hit zero times by others. Also, I am monitoring the temp directory where I save the logs; so when I send a request and see a new log file is added I am sure my request did hit the expected server/endpoint, and that the log does pertain to that request. – Majid Fouladpour Mar 13 '19 at 11:40
  • @sideshowbarker I did that in update 2. – Majid Fouladpour Mar 13 '19 at 11:41
  • just FYI - I notice you use debug.dev - I had all kinds of problems with .dev when that extension was purchased and became a real thing like .io – Tarek Adam Mar 15 '19 at 20:03

2 Answers2

3

The issue could be similar as my answer here:

The server is not configured to respond to OPTIONS requests with the correct "Access-Control-Allow-" headers.

Opening the url in a new Tab is a GET request and is working because it is not making a preflight request, as it meets the criteria to be a simple request as defined by the CORS documentation

On the other hand, the ajax request is a POST request and meets the criteria to be a Preflighted request, meaning a preflight OPTIONS request should be made first.

In short, you have correctly setup the CORS response headers, but the server is not configured to add these headers for OPTIONS method requests.

The solution is to handle OPTIONS requests on the server code with a 2xx response and add the **Access-Control-Allow- as you do for GET and POST requests. Keep in mind that OPTIONS requests do not include any parameters, so this should be done before any validation or request parsing.

Moreover, according to Access-Control-Allow-Origin documentation:

If the server specifies a single origin rather than the "*" wildcard, then the server should also include Origin in the Vary response header — to indicate to clients that server responses will differ based on the value of the Origin request header.

So set the Vary response header also:

eg at the top of you script try:

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
    header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
    header("Access-Control-Allow-Credentials: true");
    header("Vary: Origin");
    exit;
}

References

Preflighted requests

response for preflight 403 forbidden

Jannes Botis
  • 11,154
  • 3
  • 21
  • 39
2

The problem is that you use header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']); (BTW: not very recommended) and you get the wrong header. I bet on one of the reasons:

  • The web server overwrites the headers.
  • The browser uses cache, although it should not. (BFC).
  • You did not upload the updated code to the requesting server.

Let's start from the end.

  • Do not believe herself, verify. (Not once did I look, we do not work, and then came the reflection that I did not upload changes :) ).
  • Turn off the cache in the browser debugger. (you must have open debugger) If this is a problem and the function is to be available in the pool, add timestamp to the request.
  • Check the configuration of nginx / apache / server panel

Are you aware that the construction used by you is synonymous with Access-Control-Allow-Origin: *? You should check HTTP_ORIGIN if it belongs to the allowed pool.

bato3
  • 2,695
  • 1
  • 18
  • 26