-1

I'm calling through Axios a PHP script checking whether a URL passed to it as a parameter can be embedded in an iframe. That PHP script starts with opening the URL with $_GET[].

Strangely, a page with cross-origin-opener-policy: same-origin (like https://twitter.com/) can be opened with $_GET[], whereas a page with Referrer Policy: strict-origin-when-cross-origin (like https://calia.order.liven.com.au/) cannot.

I don't understand why, and it's annoying because for the pages that cannot be opened with $_GET[] I'm unable to perform my checks on them - the script just fails (meaning I get no response and the Axios call runs the catch() block).

So basically there are 3 types of pages: (1) those who allow iframe embeddability, (2) those who don't, and (3) the annoying ones who not only don't but also can't even be opened to perform this check.

Is there a way to open any page with PHP, and if not, what can I do to prevent my script from failing after several seconds?

PHP script:

$source = $_GET['url'];
$response = true;

try {
  $headers = get_headers($source, 1);
  $headers = array_change_key_case($headers, CASE_LOWER);

  if (isset($headers['content-security-policy'])) {
    $response = false;
  }
  else if (isset($headers['x-frame-options']) &&
    $headers['x-frame-options'] == 'DENY' ||
    $headers['x-frame-options'] == 'SAMEORIGIN'
  ) {
    $response = false;
  }
} catch (Exception $ex) {
  $response = $ex;
}

echo $response;

EDIT: below is the console error.

Access to XMLHttpRequest at 'https://path.to.cdn/iframeHeaderChecker?url=https://calia.order.liven.com.au/' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
CustomLink.vue?b495:61 Error: Network Error
    at createError (createError.js?2d83:16)
    at XMLHttpRequest.handleError (xhr.js?b50d:84)
VM4758:1 GET https://path.to.cdn/iframeHeaderChecker?url=https://calia.order.com.au/ net::ERR_FAILED
drake035
  • 3,955
  • 41
  • 119
  • 229
  • *"the script just fails"* Which means? How do you know it fails? – Olivier Apr 20 '21 at 08:02
  • Sorry I mean I don't get any response and the Axios call moves to the `catch()` block. I'll edit the question to make that more clear, thanks. – drake035 Apr 20 '21 at 10:44
  • Please provide the message and/or type of the exception. We can't guess it. – Olivier Apr 20 '21 at 10:50
  • You're right, I added that information in the question. – drake035 Apr 20 '21 at 14:57
  • So, if we talking about the PHP part, there's probably a server rule on their side which is blocking requests that are not specifying specific headers. It can be something like AntiDDoS protection or etc. So you can try to modify your request headers (like take em carefully from your browser) and pretend to be a real visitor to pass this protection. https://www.php.net/manual/en/function.stream-context-create.php – Electronick Apr 20 '21 at 16:13
  • The error you added in the question is the browser blocking the call to your script. It should always happen, whatever the value of `url`. – Olivier Apr 21 '21 at 07:32
  • `else if` is one word in php, please consistently use `elseif`. – mickmackusa Apr 23 '21 at 10:10
  • @drake035 Does my answer help? – Don't Panic Apr 25 '21 at 10:18
  • It is weeks later but I keep wondering, why didn't you award the bonus? 150 points is a big bonus on SO, 3x the standard, which suggests you were *very* keen for a solution. Did neither of the answers help at all? If not why not let us know so we could continue trying to help? – Don't Panic May 18 '21 at 12:28
  • @Don'tPanic You're right, and I apologize for this. This whole thing was suddenly removed as a requirement from our project and this thread slipped through the cracks. I'll create a new bounty (minimum for second bounty is 300 reputation points) and accept your answer as soon as allowed by SO. Let me just ask you one little thing before I do so: can I add `header("Access-Control-Allow-Origin: *");` anywhere in my PHP script or does it have to be at the very beginning for example? – drake035 May 27 '21 at 11:30
  • Ah no worries, I was just surprised. Yes you can add `header()` anywhere - but it has to be the first *output* your script generates ([watch out for whitespace](https://www.php.net/manual/en/function.header.php) before your opening ` – Don't Panic May 28 '21 at 06:58
  • OK, thanks! (just waiting the required 23h from now to grant bounty) – drake035 May 28 '21 at 09:10
  • Wow, that is generous! Thank you. – Don't Panic May 28 '21 at 11:59

2 Answers2

3

The error you have shown is coming from Javascript, not from PHP. get_headers() returns false on failure, it will not throw an exception - the catch() never happens. get_headers() just makes an http request, like your browser, or curl, and the only reason that would fail is if the URL is malformed, or the remote site is down, etc.

It is the access from http://localhost:3000 to https://path.to.cdn/iframeHeaderChecker with Javascript that has been blocked, not PHP access to the URLs you are passing as parameters in $_GET['url'].

What you're seeing is a standard CORS error when you try to access a different domain than the one the Javascript is running on. CORS means Javascript running on one host cannot make http requests to another host, unless that other host explicitly allows it. In this case, the Javascript running at http://localhost:3000 is making an http request to a remote site https://path.to.cdn/. That's a cross-origin request (localhost !== path.to.cdn), and the server/script receiving that request on path.to.cdn is not returning any specific CORS headers allowing that request, so the request is blocked.

Note though that if the request is classed as "simple", it will actually run. So your PHP is working already, always, but bcs the right headers aren't returned, the result is blocked from being displayed in your browser. This can lead to confusion bcs for eg you might notice a delay while it gets the headers from a slow site, whereas it is super fast for a fast site. Or maybe you have logging which you see is working all the time, despite nothing showing up in your browser.

My understanding is that https://path.to.cdn/iframeHeaderChecker is your PHP script, some of the code of which you have shown in your question? If so, you have 2 choices:

  1. Update iframeHeaderChecker to return the appropriate CORS headers, so that your cross-origin JS request is allowed. As a quick, insecure hack to allow access from anyone and anywhere (not a good idea for the long term!) you could add:

    header("Access-Control-Allow-Origin: *");
    

    But it would be better to update that to more specifically restrict access to only your app, and not everyone else. You'll have to evaluate the best way to do that depending on the specifics of your application and infrastructure. There many questions here on SO about CORS/PHP/AJAX to check for reference. You could also configure this at the web server level, rather than the application level, eg here's how to configure Apache to return those headers.

  2. If iframeHeaderChecker is part of the same application as the Javascript calling it, is it also available locally, on http://localhost:3000? If so, update your JS to use the local version, not the remote one on path.to.cdn, and you avoid the whole problem!

Don't Panic
  • 13,965
  • 5
  • 32
  • 51
0

This is just my rough guess about what wrong with your code can be.

I noticed you do:

a comparison of values from $headers but without ensuring they have the same CAPITAL CASE as the values you compare against. Applied: strtoupper().

check with isset() but not test if key_exist before Applied: key_exist()

check with isset() but perhaps you should use !empty() instead of isset() compare result:

$value = "";
var_dump(isset($value)); // (bool) true
var_dump(!empty($value)); // (bool) false

$value = "something";
var_dump(isset($value)); // (bool) true
var_dump(!empty($value)); // (bool) true

unset($value);
var_dump(isset($value)); // (bool) false
var_dump(!empty($value)); // (bool) false

The code with applied changes:

<?php

error_reporting(E_ALL);
declare(strict_types=1);

header('Access-Control-Allow-Origin: *');

ob_start();

try {

    $response = true;

    if (!key_exists('url', $_GET)) {
        $msg = '$_GET does not have a key "url"';
        throw new \RuntimeException($msg);
    }
    $source = $_GET['url'];

    if ($source !== filter_var($source, \FILTER_SANITIZE_URL)) {
        $msg = 'Passed url is invaid, url: ' . $source;
        throw new \RuntimeException($msg);
    }

    if (filter_var($source, \FILTER_VALIDATE_URL) === FALSE) {
        $msg = 'Passed url is invaid, url: ' . $source;
        throw new \RuntimeException($msg);
    }

    $headers = get_headers($source, 1);

    if (!is_array($headers)) {
        $msg = 'Headers should be array but it is: ' . gettype($headers);
        throw new \RuntimeException($msg);
    }

    $headers = array_change_key_case($headers, \CASE_LOWER);

    if (  key_exists('content-security-policy', $headers) &&
            isset($headers['content-security-policy'])
        ) {
            $response = false;
    }
    elseif (  key_exists('x-frame-options', $headers) && 
                (
                    strtoupper($headers['x-frame-options']) == 'DENY' ||
                    strtoupper($headers['x-frame-options']) == 'SAMEORIGIN'
                )
    ) {
            $response = false;
    }

} catch (Exception $ex) {
    $response = "Error: " . $ex->getMessage() . ' at: ' . $ex->getFile() . ':' . $ex->getLine();
}

$phpOutput = ob_get_clean();
if (!empty($phpOutput)) {
    $response .= \PHP_EOL . 'PHP Output: ' . $phpOutput;
}

echo $response;

Using Throwable instead of Exception will also catch Errors in PHP7.

Keep in mind that:

$response = true;
echo $response; // prints "1"

but

$response = false;
echo $response; // prints ""

so for the $response = false you'll get an empty string, not 0 if you want to have 0 for false and 1 for true then change the $response = true; to $response = 1; for true and $response = false; to $response = 0; for false everywhere.

I hope that somehow helps

Jimmix
  • 5,644
  • 6
  • 44
  • 71
  • Thanks assuming the problem is caused by some of these points, should I not still get a response from the call? (in my case it goes straight to the `catch()` block for certain pages) – drake035 Apr 20 '21 at 10:47
  • @drake035 That's probably because you have an invalid url passed or by any other error. You need to share with us the output of the exception - it should exactly point out where the problem was. Not seeing expected results may be also related to the fact that `echo false` prints an empty string instead of `0` whereas `echo true` prints `1` see my answer's update. – Jimmix Apr 20 '21 at 15:18
  • thanks, I added the exception output in my question. – drake035 Apr 20 '21 at 18:01
  • @drake035 You may need to set this header `Access-Control-Allow-Origin: *` on your web server as well if the page that requests your php script is not generated by PHP. – Jimmix Apr 20 '21 at 19:28
  • @drake035 However if the page is generated with PHP you need to add `header('Access-Control-Allow-Origin: *');` in the main page that generates output. – Jimmix Apr 22 '21 at 19:32
  • Why not `in_array(strtoupper($headers['x-frame-options']), ['DENY', 'SAMEORIGIN'])`? – mickmackusa Apr 23 '21 at 07:20
  • `key_exists('content-security-policy', $headers) && isset($headers['content-security-policy'])` is unnecessarily redundant -- in this case just you `isset()`. `invalid` has an `l` in it. `$phpOutput` will be declared, so `!empty()` is doing more work than necessary, just do a function-less truthy check. – mickmackusa Apr 23 '21 at 10:07