Missing CORS headers in response from PHP/Apache2
Asked Answered
M

3

11

A Zend Expressive project my company is working on is ready to be shipped but in our staging environment we seem to be missing response headers for a CORS pre-flight request. This does not happen in our development environment. We're using CorsMiddleware in our pipeline but it doesn't look like that middleware is the culprit.

The problem

During runtime, the middleware detects incoming pre-flight requests and it will reply with a response like so:

HTTP/1.1 200 OK
Date: Mon, 20 Aug 2018 15:09:03 GMT
Server: Apache
X-Powered-By: PHP/7.1.19
Access-Control-Allow-Origin: https://example.com
Vary: Origin
Access-Control-Allow-Headers: content-type
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

Well, that only works on our development servers and php's built-in webservers. The response is different from our staging server, even though the request is exactly the same, apart from the host:

HTTP/1.1 200 OK
Date: Mon, 20 Aug 2018 15:11:29 GMT
Server: Apache
Keep-Alive: timeout=5, max=100
Cache-Control: max-age=0, no-cache
Content-Length: 0
Content-Type: text/html; charset=UTF-8

What we've tried

Investigating the middleware

We've verified that CorsMiddleware runs perfectly fine and actually sets the required headers. When we modify CorsMiddleware's response code and set it to 202 instead of 200 we now do get the headers we're looking for. Changing the response code back to 200 makes the headers disappear again.

Setting the headers manually

Using the following example:

header('Access-Control-Allow-Origin: https://example.com');
header('Access-Control-Allow-Headers: content-type');
header('Vary: Origin');
exit(0);

This has the same behavior until we modify the response code to 204 or anything other than 200.

Looking at the body

The response body is empty and shouldn't contain anything but when we add content to the response body the headers appear as if nothing was wrong.

So if I add body content, the headers are present. No body content? No CORS headers. Is this some setting in Apache? Am I missing some configuration in PHP? Am I forgetting anything?

Further details

All requests have been tested with httpie, Postman, curl and PhpStorm's http client.

Here's the httpie example:

http -v OPTIONS https://staging.****.com \
    'access-control-request-method:POST' \
    'origin:https://example.com' \
    'access-control-request-headers:content-type'

Here's the curl example:

curl "https://staging.****.com" \
--request OPTIONS \
--include \
--header "access-control-request-method: POST" \
--header "origin: https://example.com" \
--header "access-control-request-headers: content-type"

Cors configuration in pipeline.php (wildcard only for testing):

$app->pipe(new CorsMiddleware([
    "origin"         => [
        "*",
    ],
    "headers.allow"  => ['Content-Type'],
    "headers.expose" => [],
    "credentials"    => false,
    "cache"          => 0,

    // Get list of allowed methods from matched route or provide empty array.
    'methods' => function (ServerRequestInterface $request) {
        $result = $request->getAttribute(RouteResult::class);
        /** @var \Zend\Expressive\Router\Route $route */
        $route = $result->getMatchedRoute();

        return $route ? $route->getAllowedMethods() : [];
    },

    // Respond with a json response containing the error message when the CORS check fails.
    'error'   => function (
        ServerRequest $request,
        Response $response,
        $arguments
    ) {
        $data['status']  = 'error';
        $data['message'] = $arguments['message'];

        return $response->withHeader('Content-Type', 'application/json')
                        ->getBody()->write(json_encode($data));
    },
]);

The staging environment:

OS: Debian 9.5 server
Webserver: Apache/2.4.25 (Debian) (built: 2018-06-02T08:01:13)
PHP: PHP 7.1.20-1+0~20180725103315.2+stretch~1.gbpd5b650 (cli) (built: Jul 25 2018 10:33:20) ( NTS )

Apache2 vhost on staging:

<IfModule mod_ssl.c>
<VirtualHost ****:443>
        ServerName staging.****.com
        DocumentRoot /var/www/com.****.staging/public

        ErrorLog /var/log/apache2/com.****.staging.error.log
        CustomLog /var/log/apache2/com.****.staging.access.log combined
        <Directory /var/www/com.****.staging>
                Options +SymLinksIfOwnerMatch
                AllowOverride All
                Order allow,deny
                allow from all
        </Directory>
SSLCertificateFile /etc/letsencrypt/live/staging.****.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/staging.****.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

Apache2 vhost on development:

<VirtualHost *:443>
        ServerName      php71.****.com
        ServerAdmin     dev@****.com
        DocumentRoot    /var/www/

        <Directory /var/www/>
                Options Indexes FollowSymlinks
                AllowOverride All
                Require all granted
        </Directory>

        ErrorLog        ${APACHE_LOG_DIR}/error.ssl.log
        CustomLog       ${APACHE_LOG_DIR}/access.ssl.log combined

        SSLEngine On
        SSLCertificateFile /etc/ssl/certs/****.crt
        SSLCertificateKeyFile /etc/ssl/certs/****.key
</VirtualHost>

To everybody pointing fingers to Cloudflare:

Try this direct link with httpie. This link is not using cloudflare:

http -v OPTIONS http://37.97.135.33/cors.php \
    'access-control-request-method:POST' \
    'origin:https://example.com' \
    'access-control-request-headers:content-type'

Check the source code in your browser: http://37.97.135.33/cors.php?source=1

Mm answered 20/8, 2018 at 15:48 Comment(13)
Can you show the vhost for both environments? Could the header be overridden by apache?Ryon
@JimWright I've added both vhosts in my post.Mm
Did you try to add Header set Access-Control-Allow-Origin "*" in apache?Parmenter
You can take a look here to help for configuration : benjaminhorn.io/code/…Bushtit
Do you know the source of that added Cache-Control: max-age=0, no-cache response header in staging?Papaverine
I suggest logging %{Access-Control-Allow-Origin}o to be 100% certain these headers aren't tinkered with after leaving Apache. I would also want to verify the status code logged is what you expect (and especially not 304)Papaverine
You mean the headers are perfect on the staging server when the body carries some data but not when there is no data in the body?Turman
How is PHP executed on these systems? mod_php or via FCGI/php-fpm?Parfitt
I'd look at apache's mod_headers settings for the staging server, it could well be configured to strip "unsafe" headers, usually things like x-powered-by, which appears to be the case judging from your results. It's also possible to configure upstream proxys to do the same, if staging is running behind a proxy that could be the culprit too.Dannydannye
@Dannydannye Sorry, I've just posted an answer which is essentially the same as your comment. Do you wish to post this answer yourself? If so, let me know so I can retract mine.Erythromycin
I believe I notice that your development response shows php as 7.1.19, yet staging seems to be at 7.1.20. Do you have a practical / doable way of setting staging on 7.1.19 (as in dev) to make sure something in 7.1.20 is not in cause?Impresario
Have you seen this? bz.apache.org/bugzilla/show_bug.cgi?id=51223Robichaud
@Robichaud That's very interesting! But if I understand correctly, it only happens upon 304 status codes?Mm
B
2

From everything I read here, including your comments it seems your "production" server is behind a PROXY more exactly CloudFlare. You have given details about your working development envinroment, but nothing about the non-working production environment.

Your setup seems correct, and if it does work on a development setup without a PROXY, it means that the PROXY is modifying the headers.

A quick search about this regarding CloudFlare has given enough indication that CloudFlare can be the cause of your problem.

I strongly suggest you enable "Development Mode" in CloudFlare so it will bypass the cache and you can see everything coming/going to the origin server.

The following article should help you understand and resolve your issue:

https://support.cloudflare.com/hc/en-us/articles/203063414-Why-can-t-I-see-my-CORS-headers-

UPDATE:

It appears that your issue is from Apache Mod Pagespeed, by turning it off your headers are present all times.

It is still unclear why the mod is stripping your headers, but that's for another question and time.

Bedder answered 30/8, 2018 at 20:29 Comment(8)
Thanks for your input, but you clearly misunderstand. Production is behind Cloudflare. Staging is NOT behind any kind of proxy.Mm
Exactly so you said that STAGING is working OK and PRODUCTION is NOT, or am I wrong ?Bedder
I had a typo... instead of "staging" I meant "production" where I said "From everything I read here, including your comments it seems your "production" server is behind a PROXY more exactly CloudFlare." ...Bedder
Check the OP, I've added a direct link to the scenario that's happening, including a link to view the source code. It has nothing to do with Cloudflare.Mm
The direct link is not using Cloudflare.Mm
Yes, but I do not see any header set from your PHP. So how is this "working" as you stated in your OP ? Anyway... can you add print_r(headers_list()); before the exit(0); ?Bedder
Add if you can print_r(apache_request_headers()); too, without the highlight so we can see the headers... if they are being set properly or not.Bedder
Let us continue this discussion in chat.Mm
E
1

Your configuration makes it clear that the headers do get generated, so it's not the code or the middleware's fault.

I believe that the headers get removed by something - check Apache's mod_headers and configuration in case there's a rogue unset directive.

Another, less likely possibility is that you're looking at the staging server through a load balancer or proxy of some kind, which rewrites the headers and leaves the CORS out (to verify that, you might need to intercept Apache's outgoing traffic).

I have made both mistakes, myself.

Erythromycin answered 28/8, 2018 at 17:13 Comment(4)
Plausible possible explanation. If this is the case, I'd be interested in why mod_headers/whatever only removes the headers for status code 200 responses, but lets those with 202/204 through.Parfitt
Thanks for your input. I just checked and mod_headers is not even enabled. Production environment is behind Cloudflare, but staging is serving directly without any kind of load balancer or proxy.Mm
Can you check with a simple PHP page that just sets those headers and exits, without anything else in the middle? Ideally you could temporarily replace Apache with a fake "web server" using netcat.Erythromycin
@Erythromycin I did. Check the OP, I've added a direct link to the scenario that's happening, including a link to view the source code.Mm
T
0

Please make sure you have the right configuration in Zend Expressive. For example the code below will allow CORS access to any calling domain

use Psr\Http\Message\ServerRequestInterface;
use Tuupola\Middleware\CorsMiddleware;
use Zend\Expressive\Router\RouteResult;

$app->pipe(new CorsMiddleware([
    "origin" => ["*"],
    "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"]
}
]));
Turman answered 25/8, 2018 at 17:1 Comment(7)
In our case the POST method is required, so PUT is not an option.Mm
Could you share the Zend Expressive CORS configuration code? I believe you have a configuration issue which is leading to this problem.Turman
Did you make any changes to the htaccess file inside the public folder?Turman
.htaccess file is untouched, otherwise the problem would probably occur on both environments. CORS configuration has been added to OP.Mm
Why wont you just change the methods to this "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"], and send back your feedbackTurman
Check the OP, I've got a minimal but working example right there with my feedback.Mm
Check the OP, I've added a direct link to the scenario that's happening, including a link to view the source code.Mm

© 2022 - 2024 — McMap. All rights reserved.