0

I am generating a unique token and saving it in a session variable on every request (in a typical CSRF protection fashion). Token is refreshed after checking it for validation with the POSTED token value.

Here is my code (index.php):

<?php
    
session_start();

if (!empty($_POST['token'])) {
    var_dump($_POST['token'], $_SESSION['token']);
    exit;
}

$_SESSION['token'] = rand();

echo '<form action="index.php" method="post"><input name="token" value="' . $_SESSION['token'] . '"></form>';

When I use php -S localhost:8888 to run the script, it works fine. But when I specify the index.php file like php -S localhost:8888 index.php the $_SESSION['token'] is changed. ($_POST['token'] and $_SESSION['token'] does not match).

php -S localhost:8888

before after

php -S localhost:8888 index.php

before after

I have also tried using a routing file. It does not work either. php -S localhost:8888 server.php

<?php
// server.php

$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = urldecode($uri);

if ($uri !== '/' and file_exists($uri))
{
    return false;
}

require_once 'index.php';

Console output:

php -S localhost:8888

php -S localhost:8878 
[Mon Mar 29 11:49:49 2021] PHP 8.0.3 Development Server (http://localhost:8878) started 
[Mon Mar 29 11:49:52 2021] [::1]:47410 Accepted 
[Mon Mar 29 11:49:52 2021] [::1]:47412 Accepted 
[Mon Mar 29 11:49:52 2021] [::1]:47410 [200]: GET / 
[Mon Mar 29 11:49:52 2021] [::1]:47410 Closing 
[Mon Mar 29 11:49:53 2021] [::1]:47412 [404]: GET /favicon.ico - No such file or directory 
[Mon Mar 29 11:49:53 2021] [::1]:47412 Closing

php -S localhost:8888 server.php

php -S localhost:8858 server.php
[Mon Mar 29 11:48:51 2021] PHP 8.0.3 Development Server (http://localhost:8858) started 
[Mon Mar 29 11:48:53 2021] [::1]:33156 Accepted 
[Mon Mar 29 11:48:53 2021] [::1]:33158 Accepted 
[Mon Mar 29 11:48:53 2021] [::1]:33156 Closing 
[Mon Mar 29 11:48:54 2021] [::1]:33158 Closing

Tested using:

PHP 7.3.27-1~deb10u1 (cli) (built: Feb 13 2021 16:31:40) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.27, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.27-1~deb10u1, Copyright (c) 1999-2018, by Zend Technologies

and

PHP 8.0.3 (cli) (built: Mar  5 2021 08:38:30) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.3, Copyright (c) Zend Technologies
    with Zend OPcache v8.0.3, Copyright (c), by Zend Technologies

Is this a bug in PHP built-in server?

adhm
  • 1
  • 1
  • 2
  • 1
    Why would you pass a file to the server in this scenario? – Steven Mar 28 '21 at 09:44
  • @Steven Why not? It shouldn't make a difference, should it? Is there a downside to passing `index.php`? – brombeer Mar 28 '21 at 10:03
  • @brombeer yes, passing a file with `-S` means the file acts as a router file and is expected to return content OR `false` – Steven Mar 28 '21 at 10:06
  • @Steven But that script _is_ returning content? – brombeer Mar 28 '21 at 10:10
  • @brombeer yes it does and indeed technically I would expect this code to work. But it doesn't. So understanding why the OP is doing things the way they are (which changes the behaviour of PHP) seems like a sensible place to start... For refence my second question will be "how is the form being submitted?" – Steven Mar 28 '21 at 10:42
  • On a LinuxMint machine, using `php -S localhost:8000 index.php` resulting in `[Sun Mar 28 12:47:11 2021] PHP 7.4.16 Development Server (http://localhost:8000) started` this code runs fine and the error _cannot_ be reproduced. Same for port 8888 – brombeer Mar 28 '21 at 10:50
  • @Steven Yeah, would think so too and as my prev comment says I cannot reproduce the behavior on my machine. "how is the form being submitted?" would've also been my next question. – brombeer Mar 28 '21 at 10:52
  • @Steven You can hit enter on the displayed text field or you can add a submit button in the HTML form. Should not make a difference. – adhm Mar 28 '21 at 12:06
  • I tried it with a router file as well. Same results. – adhm Mar 28 '21 at 12:08
  • @brombeer interesting that you cannot reproduce. I will attempt the same although I maintain that it is (/has to be) some quirk with the configuration line setting `index.php` as a routing file – Steven Mar 28 '21 at 12:49
  • @adhm I'm not sure what you mean by that. Your second call line (the one which doesn't work) **is** using `index.php` as a routing file – Steven Mar 28 '21 at 12:50
  • @Steven I tried with a router file as well. It does not seem to work. I have updated the question. – adhm Mar 29 '21 at 06:46

1 Answers1

1

Routing files

This is caused because of the way you have your options set in your command to php: you set the routing file to be index.php`...

php -S localhost:8888 index.php
                      ^^^^^^^^^

...this means that every request will go via that file first and then decide what to do. For example, suppose our index.php file contains the following:

if (rand(1,100) % 2) {
    echo "Not a multiple of 2\n";
    return true;
} else {
    echo "Multiple of 2\n";
    return false;
}

Note: return true; technically isn't needed but I've included it for clarity

What happens here is that the request is made and the routing file is run. If the routing file returns false (i.e. is a Multiple of 2) then the request goes through to the requested file. If the code returns true then execution stops and the requested file isn't accessed.

In your case the requested file is index.php (the same file) so, with the above code, you will always end up with output like:

Not a multiple of 2

// OR

Multiple of 2
Multiple of 2

// OR 

Multiple of 2
Not a multiple of 2

Break down of the http request with your code/setup

So, looking at your actual code...

session_start();

if (!empty($_POST['token'])) {
    var_dump($_POST['token'], $_SESSION['token']);
    exit;
}

$_SESSION['token'] = rand();

echo '<form ...>...</form';

What happens here?

  1. You make a request for index.php*
  2. The server accesses the routing file index.php to decide what to do
  3. The routing file outputs data which effectively indicates true
  4. The data (form) from the routing file is returned & the SESSION variable has been set
    • The request for a file is ignored
  5. You submit the form
  6. The request goes through to the routing file to decide what to do (NOT the location of the attribute action**)
  7. The routing file checks the POST>token against SESSION>token.
  8. They should match and so the routing file effectively outputs data which indicates true***
  9. The data is returned
    • The request for a file (from the action attribute) is ignored

* It makes 0 difference what file you attempt to access; try it with infkdsngfdslghfdslgnfdg.php and it'll still work the same way. Your requested file is never accessed it only appears that it is because the requested and routed file are the same!

** As above, you can set the action attribute to almost anything, try fdsfnldgksdf.php

*** Whether the tokens match or not the routing file still outputs data which equates to true

As per @brombeer's test this does work as expected, so why doesn't it work for you?

The problem

If you check the command prompt/terminal where the server is running you get a stream of what is happening (e.g. when a request is a accepted etc.). You'll notice that if you watch that when making a request you get results like:

[DATE] [::1]:XXX01 Accepted
[DATE] [::1]:XXX02 Accepted
[DATE] [::1]:XXX01 Closing
[DATE] [::1]:XXX02 Closing

@brombeer on the other hand will get results like:

[DATE] [::1]:XXX01 Accepted
[DATE] [::1]:XXX01 Closing

This is the problem. You're making two requests to @brombeer's one request and both requests go through the routing file.

The first request is the one you expect and you get output as you would expect. However, after you receive that output the second request runs (which follows the exact same flow as described above - remember it doesn't matter what file is requested the script will output the same thing(!) - and effectively changes the $_SESSION["token"] to a new random number.

This can perhaps be seen more easily if you change...

$_SESSION["token"] = rand();

...to...

$_SESSION["token"]++;

What is the second request?

This is nothing to do with PHP; it's all to do with your browser. Browsers make requests for all sorts of things other than the requested file. For example:

  • JavaScript files
  • CSS files
  • Images used on the page

Of course, you aren't using any of that in this example. However, there are some resources that browsers look for whether you tell them to or not: usually based on context.

In this case your browser is smart enough to know that you're trying to access a website (likely because of the port number but maybe because of the request method or URI).

So it tries to locate some additional files that it would expect to find on a website, specifically: favicon.ico (you should be able to see this request in your bowser's dev tools under Network).

As already explained, because you've set your server up with a routing file that request goes through the same exact process as the index.php or fnjksgjfndsglkjnsf.php requests. In fact the actual icon file is never even looked for.

You can prove this further by adding this code to the top of your file...

if (!strpos($_SERVER["REQUEST_URI"], ".php")) {
    return false;
}

This will stop execution in your routing file if the requested file isn't a php file. Additionally because we return false the server will look for the icon file. Returning true would also work but the icon file wouldn't be looked for.

You could also try changing the port to something like :8030 and I expect the code would work as you expect (because the browser won't request a favicon).

The solution

Referencing back to my first comment on the question...

Why would you pass a file to the server in this scenario?

I'm still not sure why you've done it: I assume that it's because you didn't understand what a routing file did? Or perhaps that you didn't understand that you were creating a routing file?

Hopefully we've cleared that up here?

Either way I am fairly certain that setting up a routing file isn't what you intended and for your purposes it doesn't appear that it's what you need either.

So just don't add index.php to the end of the command.


Additional worked example

Replace your index.php with the following code:

session_start();

echo "<pre>";

if (!empty($_POST['token'])) {
    var_dump($_POST['token'], $_SESSION['token']);
    exit;
}

$_SESSION['token']++;

echo '<form action="indasdasfdex.php" method="post"><input name="token" value="' . $_SESSION['token'] . '"><input type="submit" value="submit"></form>';

var_dump($_POST['token'] ?? null, $_SESSION['token'] ?? null);

Run your server with index.php as the router file (as per your original question):

php -S localhost:8888 index.php

// Because you require `index.php` in your `server.php`
// this will work the same if you use `server.php` instead

This is what happens

Request: index.php

Router file runs `$_SESSION["token"] == 1`
Router file returns data to browser with a form: `"token" == 1`
Request terminated
Browser shows returned data
    Form: `token.value == 1`
    `var_dump` output
        `$_POST["token"] == null`
        `$_SESSION["token"] == 1`

Request: favicon.ico

Router file runs `$_SESSION["token"] == 2`
Router file returns data to browser with a form `"token" == 2`
Request terminated
Browser doesn't show returned data (but you can see it in dev tools)
    _Not shown because an image isn't returned!_
    _Can be seen in dev tools_
    Form: `token.value == 2`
    `var_dump` output
        `$_POST["token"] == null`
        `$_SESSION["token"] == 2`

Request: submit form to index.php

Router file runs
    Dumps `$_SERVER["token"]` and `$_POST["token"]` (2,1)
Router file `exit`
Request terminated
Browser shows returned data
    `var_dump` output
        `$_POST["token"] == 1`
        `$_SESSION["token"] == 2`

Request: favicon.ico

Router file runs `$_SESSION["token"] == 3`
Router file returns data to browser with a form `"token" == 3`
Request terminated
Browser doesn't show returned data (but you can see it in dev tools)
    _Not shown because an image isn't returned!_
    _Can be seen in dev tools_
    Form: `token.value == 3`
    `var_dump` output
        `$_POST["token"] == null`
        `$_SESSION["token"] == 3`**
Steven
  • 6,053
  • 2
  • 16
  • 28
  • Thanks for the answer. I do know what a router file is and I tried with one as well. `php -S localhost:8888 router.php` here is the contents of the server.php ` – adhm Mar 29 '21 at 06:38
  • I have edited the question to include the server.php – adhm Mar 29 '21 at 06:43
  • I get this output when using a router file. `php -S localhost:8858 server.php [Mon Mar 29 11:48:51 2021] PHP 8.0.3 Development Server (http://localhost:8858) started [Mon Mar 29 11:48:53 2021] [::1]:33156 Accepted [Mon Mar 29 11:48:53 2021] [::1]:33158 Accepted [Mon Mar 29 11:48:53 2021] [::1]:33156 Closing [Mon Mar 29 11:48:54 2021] [::1]:33158 Closing` – adhm Mar 29 '21 at 06:49
  • Without using router file. `php -S localhost:8878 [Mon Mar 29 11:49:49 2021] PHP 8.0.3 Development Server (http://localhost:8878) started [Mon Mar 29 11:49:52 2021] [::1]:47410 Accepted [Mon Mar 29 11:49:52 2021] [::1]:47412 Accepted [Mon Mar 29 11:49:52 2021] [::1]:47410 [200]: GET / [Mon Mar 29 11:49:52 2021] [::1]:47410 Closing [Mon Mar 29 11:49:53 2021] [::1]:47412 [404]: GET /favicon.ico - No such file or directory [Mon Mar 29 11:49:53 2021] [::1]:47412 Closing` – adhm Mar 29 '21 at 06:50
  • @adhm maybe I didn't make this clear. Your command line `php -S localhost:8888 index.php` uses `index.php` as a router file. That is your problem, as described in my answer. Does that make sense? – Steven Mar 29 '21 at 14:44
  • @adhm If that doesn't make sense then perhaps you could explain to me what you think/intend to happen when you pass `index.php` as part of the command line? And I can clear up any confusion. I feel like we may be talking at cross purposes otherwise! – Steven Mar 29 '21 at 14:51
  • I said using a router file does not solve the issue either. I have posted contents of my router file. And I am using `php -S localhost:8888 router.php` NOT `php -S localhost:8888 index.php` to run with the router file (YES in my question it's `php -S localhost:8888 index.php`. But using a router file `php -S localhost:8888 router.php` does not solve the issue either). Maybe something is wrong with router file. – adhm Apr 03 '21 at 07:49
  • @adhm if you're unable to grasp what the issue is then that's okay, I'm more than happy to help but you can be sure to watch your tone or that will end quickly. Yes I did _bother_ to read your comment/update. Did you _bother_ to try and understand the explanation? The answer above fully explains the problem in all scenarios you've proposed. – Steven Apr 03 '21 at 14:35
  • @adhm to be clear: when you append `index.php` as the final option you make `index.php` the router file as described above. You DO NOT want to do that. You need to leave it blank. When you attempt to use a _router file_ as with your `server.php` you get the same result because you check that the URI isn't `/` and then `require` index.php` which in all practical senses does the same thing as just using `index.php` as the router file!!! And again that causes the issue **_as described in the answer_**. – Steven Apr 03 '21 at 14:39
  • @adhm while we're on the subject did you _bother_ to attempt the proofs that I provided? Checking the dev tools and watching the additional request happen? Changing the port number and watching again? Adding the code that would prevent that from happening? Reading about router files on php.net and realising you were using it incorrectly? – Steven Apr 03 '21 at 14:43
  • Sure. So what would my corrected `server.php` would look like? I pulled the `server.php` from the Laravel Framework (and other popular frameworks I checked use a similar code). – adhm Apr 04 '21 at 06:44
  • "If a PHP file is given on the command line when the web server is started it is treated as a "router" script. The script is run at the start of each HTTP request. If this script returns false, then the requested resource is returned as-is. Otherwise the script's output is returned to the browser." from php.net. So how am I suppose to return the output without requiring the file? – adhm Apr 04 '21 at 06:49
  • @adhm I'm not entirely sure what it is that you're trying to do, you haven't explained that? But basically we can say that because you `require index.php` in the `server.php` file whether you use `server.php` or `index.php` as the router file the effect is the same. That effect being that when you access `localhost/index.php`, `index.php` is the requested file and that will be returned if the output of your router file is `false` in your scenario `index.php` will always return something and therefore the chain stops there. – Steven Apr 04 '21 at 11:50
  • The problem is that the browser makes a second request for `favicon.ico` - remember that every request goes through the router file - which effectively also accesses `index.php` in either of your setups so the `SESSION = rand` code is run again behind the scenes and so the values don't match. You do not need a router file in your scenario. – Steven Apr 04 '21 at 11:52