Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get csrf token in request and test for prefix 'http-' in csrf token header #501

Merged

Conversation

DeveloperMarius
Copy link
Contributor

@DeveloperMarius DeveloperMarius commented Mar 21, 2021

Hello Simon,

as @mmdm95 mentioned in #491 in some requests php recieve the x-csrf-token with a http- prefix.
I also have to get the header using the http- preifx. I'm using the following method to set the header in js:
jqxhr.setRequestHeader('X-CSRF-TOKEN', csrf_token);
and recieve it in the CsrfVerifier using:
$request->getHeader('HTTP-X-CSRF-TOKEN')

In this commit I added a function to the Request that returns the x-csrf-token header with and without the http- prefix.
I also added this to the BaseCsrfVerifier in the handle function.

It is explained in this stackoverflow post:

Meta-variables with names beginning with HTTP_ contain values read from the client request header fields, if the protocol used is HTTP. The HTTP header field name is converted to upper case, has all occurrences of - replaced with _ and has HTTP_ prepended to give the meta-variable name.

I'm not really sure why some people can recieve the header without the http- prefix, but I think it is better to add support.
I only added the http- prefix option as alternative and the non prefix one as original, because I think the header shoud not have this http- prefix.

~ Marius

Update:
I also added the patch method to the BaseCsrfVerifier::handle function.
Yes, the patch method does not contain any post parameters, but it will instantly fallback to the defaultValue and by that access the header.

~ Marius

@skipperbent skipperbent merged commit dfcbbb4 into skipperbent:v4-development Mar 21, 2021
@DeveloperMarius
Copy link
Contributor Author

Hello,

hier are my test results:

routes:

Router::get('/test-header', function (){
    echo <<<HTML
<script>
let xhr = new XMLHttpRequest();
xhr.open('POST', '/test-header-post');
xhr.setRequestHeader('X-CSRF-TOKEN', 'some_token');
xhr.onreadystatechange = function (){
    document.body.innerHTML = '<p>' + xhr.responseText + '</p>';
}
xhr.send();
</script>
HTML;
});
Router::post('/test-header-post', function (){
    echo $_SERVER['X_CSRF_TOKEN'] ?? 'N/A';
    echo '<br>';
    echo $_SERVER['HTTP_X_CSRF_TOKEN'] ?? 'N/A';
    echo '<br>';
    echo json_encode($_SERVER);
});

result:

N/A
some_token
{"USER":"some_user","HOME":"\/var\/www\/vhosts\/domain.de","PATH_TRANSLATED":"redirect:\/index.php\/test-header-post","PATH_INFO":"\/test-header-post","SCRIPT_NAME":"\/index.php","REQUEST_URI":"\/test-header-post","QUERY_STRING":"","REQUEST_METHOD":"POST","SERVER_PROTOCOL":"HTTP\/1.0","GATEWAY_INTERFACE":"CGI\/1.1","REDIRECT_URL":"\/test-header-post","REMOTE_PORT":"53532","SCRIPT_FILENAME":"\/var\/www\/vhosts\/domain.de\/subdomain.domain.de\/public\/index.php","SERVER_ADMIN":"root@localhost","CONTEXT_DOCUMENT_ROOT":"\/var\/www\/vhosts\/domain.de\/subdomain.domain.de\/public","CONTEXT_PREFIX":"","REQUEST_SCHEME":"https","DOCUMENT_ROOT":"\/var\/www\/vhosts\/domain.de\/subdomain.domain.de\/public","REMOTE_ADDR":"162.000.203.00","SERVER_PORT":"443","SERVER_ADDR":"185.000.99.00","SERVER_NAME":"subdomain.domain.de","SERVER_SOFTWARE":"Apache","SERVER_SIGNATURE":"","PATH":"\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin","HTTP_CF_REQUEST_ID":"08f6a6e640000041628a216000000001","HTTP_CDN_LOOP":"cloudflare","HTTP_CF_CONNECTING_IP":"46.114.0.000","HTTP_COOKIE":"__cfduid=d21b4d4d0ba9fcb8562eexxxxxxxx011616326541; PHPSESSID=a0a01f3gpandfnk1c3hf5tb85t; csrf_rs_cookie=7d82cebe1xxxxxxxx0e16447bb012","HTTP_SEC_GPC":"1","HTTP_REFERER":"https:\/\/subdomain.domain.de\/test-header","HTTP_DNT":"1","HTTP_ORIGIN":"https:\/\/subdomain.domain.de","HTTP_X_CSRF_TOKEN":"some_token","HTTP_ACCEPT_LANGUAGE":"de,en-US;q=0.7,en;q=0.3","HTTP_ACCEPT":"*\/*","HTTP_USER_AGENT":"Mozilla\/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko\/20100101 Firefox\/86.0","HTTP_CF_VISITOR":"{\"scheme\":\"https\"}","HTTP_X_FORWARDED_PROTO":"https","HTTP_CF_RAY":"6337a7506c3d4162-HAM","HTTP_ACCEPT_ENCODING":"gzip","CONTENT_LENGTH":"0","HTTP_CONNECTION":"close","HTTP_X_ACCEL_INTERNAL":"\/internal-nginx-static-location","HTTP_X_FORWARDED_FOR":"46.114.0.000","HTTP_X_REAL_IP":"162.158.000.00","HTTP_HOST":"subdomain.domain.de","proxy-nokeepalive":"1","HTTPS":"on","PASSENGER_DOWNLOAD_NATIVE_SUPPORT_BINARY":"0","PASSENGER_COMPILE_NATIVE_SUPPORT_BINARY":"0","PERL5LIB":"\/usr\/share\/awstats\/lib:\/usr\/share\/awstats\/plugins","UNIQUE_ID":"YFdPT52QAkSat6Wk2ykIvQAAAQU","REDIRECT_STATUS":"200","REDIRECT_HTTPS":"on","REDIRECT_PASSENGER_DOWNLOAD_NATIVE_SUPPORT_BINARY":"0","REDIRECT_PASSENGER_COMPILE_NATIVE_SUPPORT_BINARY":"0","REDIRECT_PERL5LIB":"\/usr\/share\/awstats\/lib:\/usr\/share\/awstats\/plugins","REDIRECT_UNIQUE_ID":"YFdPT52QAkSat6Wk2ykIvQAAAQU","FCGI_ROLE":"RESPONDER","PHP_SELF":"\/index.php\/test-header-post","REQUEST_TIME_FLOAT":1616334671.461315,"REQUEST_TIME":1616334671}

@DeveloperMarius
Copy link
Contributor Author

Hello,

okay, so I think you deleted your request for a test.
Thank you for the merge.

~ Marius

@skipperbent
Copy link
Owner

skipperbent commented Mar 21, 2021

I was supposed to push the changes to this pull request, but i did something wrong and it created a new PR. A little messy, but it's all merged now. Nice work - added some more feature to your work 👍

I think i'll change the Request object to it parses all http- and strips the http- tags. That way we don't have to use all those checks for if the webserver are using http- or not.

Something like this in the Request::__construct would make the fallback easier.

foreach ($_SERVER as $key => $value) {
   $this->headers[strtolower($key)] = $value;
   $this->headers[str_replace(['_', 'http-'], ['-', ''], strtolower($key))] = $value;
}

It always keep a copy of the original header. But then we don't have to check if the http- variant exists all around the code. It's either there or not.

@DeveloperMarius
Copy link
Contributor Author

Yes this would be easier, but I think it can lead to some problems.
When a request(from the client) contains a header the http- prefix is prepended(I understood it like this) so that when the browser or a later instance sets a header with the same name this does not get overridden.
When we now remove this prefix, this can run into a conflict and we want to keep both headers.

Your idea with keeping the original headers is not bad, but then we have to store the same data two times.
What is when we extract all http prefix headers from the header data and then store the data in separate arrays.
One array for the custom/ http prefix header and one for the system headers.
Then create 2 functions:

getHeader(string $name) -> looks over both arrays for the header name. The parameter name gets processed by tolowercase' replace http prefix, _ replaced with -.
The functions first checks the http prefix header array (prioritizes them) and then looks over the server headers.

getServerHeader(string $name) -> same process for the parameter and this function look just over the server headers (non http prefix). Maby think more about the function name.

What do you think @skipperbent ?

~ Marius

@skipperbent
Copy link
Owner

skipperbent commented Mar 21, 2021

You're right i can see my idea causing some problems.

Two methods is more code. Both in the project and for the user. We want the project to do the work for us 99% percent of the time without us having to juggle between multiple methods (getServerHeader, getHeader).

Found a solution that might do the trick without storing both versions of the headers. This way it can store all the original headers and when calling Request::getHeader you can specify if the method should try to parse the header for you (default: yes). If enabled; getHeader will look for the http- variant if the original header is not found and visa versa (look for a non http- header if the http- variant is not found).

Example:

    /**
     * Get header value by name
     *
     * @param string $name Name of the header.
     * @param string|null $defaultValue Value to be returned if header is not found.
     * @param bool $tryParse When enabled the method will try to find the header from both from client (http) and server-side variants, if the header is not found.
     *
     * @return string|null
     */
    public function getHeader(string $name, $defaultValue = null, $tryParse = true): ?string
    {
        $name = strtolower($name);
        $header = $this->headers[$name] ?? null;

        if ($tryParse === true && $header === null) {
            if (strpos($name, 'http-') === 1) {
                // Trying to find client header variant which was not found, searching for header variant without http- prefix.
                $header = $this->headers[str_replace('http-', '', $name)] ?? null;
            } else {
                // Trying to find server variant which was not found, searching for client variant with http- prefix.
                $header = $this->headers['http-' . $name] ?? null;
            }
        }

        return $header ?? $defaultValue;
    }

@skipperbent
Copy link
Owner

Hmm.. that code in issue #491 seems to do exactly the same as the code posted above.

foreach ($_SERVER as $key => $value) {
   $this->headers[strtolower($key)] = $value;
   $this->headers[str_replace(['_', 'http-'], ['-', ''], strtolower($key))] = $value;
}

But as you said, if you for some unknown reason needed both of them, only one of them will be available as the http variant will overwrite the standard one non http one. Not sure what the chances are of people needing both though.

@DeveloperMarius
Copy link
Contributor Author

DeveloperMarius commented Mar 21, 2021

You're right i can see my idea causing some problems.

Two methods is more code. Both in the project and for the user. We want the project to do the work for us 99% percent of the time without us having to juggle between multiple methods (getServerHeader, getHeader).

Found a solution that might do the trick without storing both versions of the headers. This way it can store all the original headers and when calling Request::getHeader you can specify if the method should try to parse the header for you (default: yes). If enabled; getHeader will look for the http- variant if the original header is not found and visa versa (look for a non http- header if the http- variant is not found).

Example:

    /**
     * Get header value by name
     *
     * @param string $name Name of the header.
     * @param string|null $defaultValue Value to be returned if header is not found.
     * @param bool $tryParse When enabled the method will try to find the header from both from client (http) and server-side variants, if the header is not found.
     *
     * @return string|null
     */
    public function getHeader(string $name, $defaultValue = null, $tryParse = true): ?string
    {
        $name = strtolower($name);
        $header = $this->headers[$name] ?? null;

        if ($tryParse === true && $header === null) {
            if (strpos($name, 'http-') === 1) {
                // Trying to find client header variant which was not found, searching for header variant without http- prefix.
                $header = $this->headers[str_replace('http-', '', $name)] ?? null;
            } else {
                // Trying to find server variant which was not found, searching for client variant with http- prefix.
                $header = $this->headers['http-' . $name] ?? null;
            }
        }

        return $header ?? $defaultValue;
    }

I would do it like you posted it here.
But I think you have to check if strpos === 0, because the position ist starting at 0.
It is better to include every possibility, because when you have a header with the same name (yes this would be a messy code) you can't use this project.

The possibility is small but it is possible ^^

@skipperbent
Copy link
Owner

I agree - at some point we need this in some weird use case 2 years later and then it will be confusing as of why it overwrites the header. Better to make it work properly from the beginning. Thanks for your feedback, I really appreciate it :)

@skipperbent
Copy link
Owner

And nicely spottet with that strpos :) thanks

@DeveloperMarius DeveloperMarius deleted the get-csrf-token branch November 15, 2022 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants