PHP cookie handling - php

A centain web client that I need to support, is sending back the Cookies header to my application twice in the HTTP headers, this in turn is making PHP unable to read the correct value for the cookie thus ignoring the session.
Here is the relevant part of the request I am seeing:
GET / HTTP/1.1
Cache-Control: max-age=0
Accept-Language: en-US
Cookie: PHPSESSID=49af82ddf12740e6a35b15985e93d91a
Connection: Keep-Alive
Cookie: PHPSESSID=49af82ddf12740e6a35b15985e93d91a
[...] Other irrelevant headers
I have two questions:
Is that a PHP bug? or is the behavior undefined when the client sends that same header twice?
Is there a quick workaround to make things work without having to manually parse the HTTP headers so I can read the right value of the cookie (and session) in my application? Or should I manually parse the HTTP header to set the session to its correct value?

According to the HTTP spec, a double header simply concatenates the values together with a comma, making it:
Cookie: PHPSESSID=49af82ddf12740e6a35b15985e93d91a, PHPSESSID=49af82ddf12740e6a35b15985e93d91a
PHP should be able to parse the cookies, but the behavior of sessions is undefined when there are two session IDs.
I strongly recommend fixing the client. If that's not an option, you'll have to parse the headers manually.

Related

How to set cookie in Laravel Blade File?

I want to set cookies in laravel blade.php file, not in the controller. How can I set it?
Disclaimer: I will focus my answer on PHP and laravel.
Why not set in controller?
It would really help to know why you cannot / or do not want to set cookies using laravel's cookie Facade in the controller - eg. Cookie::queue, as it's very easy to do!
Here are two ways, from this source.
Via response:
return response(view('welcome'))->cookie('name','value',$min);
Via Queue: Cookie::queue(Cookie::make('name','value',$min)); return view('welcome');
Set-Cookie is a response header, not the body!
Assuming you would set these cookies in PHP , they need to come as part of a response header, and not part of the body (view). This is why you would need to set these in the controller, where you are sending a response!
If you try to use PHP functions to set cookies, you will be met with errors "headers have already been sent"
Per the docs: https://www.php.net/setcookie
setcookie() defines a cookie to be sent along with the rest of the HTTP headers. Like other headers, cookies must be sent before any output from your script (this is a protocol restriction). This requires that you place calls to this function prior to any output, including and tags as well as any whitespace.
To understand what this means, it's helpful to understand the structure of requests and responses:
Requests and Responses are made up of headers and possibly a body.
Note: You can see these in the network tab of your browser's dev tools.
The request headers are like meta data about the request that can tell the server what kind of content is being requested, and who is requesting.
The response headers are like meta data about the response returned that can tell the server what kind of content is being delivered, how long to cache it for, associated cookies that got set.
Example Request Headers:
Content-Type: 'application/json'
Content-Type: 'application/pdf'
Content-Type: 'text/html'
Content-Type: 'text/css'
User-Agent: 'Mozilla/5.0 (<system-information>) <platform> (<platform-details>) <extensions>'
Authorization: 'Bearer <token>'
Example Response Headers:
Content-Type as it may differ from what was requested
Expires: 'Wed, 07 Sep 2022 19:26:49 GMT'
Cross-Origin-Resource-Policy: 'cross-origin'
Date: 'Wed, 07 Sep 2022 19:26:49 GMT'
Content-Length: 0,
Set-Cookie: test_cookie=CheckForPermission; expires=Wed, 07-Sep-2022 19:41:49 GMT; path=/; domain=.doubleclick.net; Secure; HttpOnly; SameSite=none
Notably: - Set-Cookie - tells the browser to add these cookies to application storage (you can view these in application / storage tabs in dev tools)
The response header can have Set-Cookie, not the request header. This makes sense, as usually the cookie information is going to come from the "answer" (response) to the "question" (request) by way of performing some logic, eg - this user is authenticated, here's a cookie to keep their session in place.
Also: Secure & HTTP only Cookies
Cookies can get set with a few options - secure only, and http only. These mean that the cookie must be Set on secure connections (https) and the http only can come from a response and cannot be overridden by JavaScript adjusting (client side)
Example of options for Laravel's Cookie::queue facade:
// $name, $value, $minutes = 0, $path = null, $domain = null, $secure = false, $httpOnly = true
Cookie::queue($name, $value, $ttl, $path, $domain, $secure, $httpOnly);
ttl = "time to live" or how long until it expires eg. 2 minutes

php setcookie behavior with cookies disabled

can anyone confirm the behavior of PHP's setcookie() function when the client has cookies disabled? According to the documentation:
"If output exists prior to calling this function, setcookie() will fail and return FALSE. If setcookie() successfully runs, it will return TRUE. This does not indicate whether the user accepted the cookie."
I'm not sure what 'successfully runs' means exactly, but this leads me to believe that the implementation doesn't care about whether the client accepts the cookie, and that we shouldn't have to worry about PHP errors / warnings related to the cookie actually being set or not. Is that right?
Thanks in advance
Cookies are sent via http header. Headers can ALWAYS be sent. Whether they're accepted/ignored is irrelevant - you can send ANY header you want.
The only way to tell if a client has accepted a cookie is if the cookie gets sent BACK to the server by the client on its NEXT request.
The only way setcookie() fails is if output has already started. That causes the PHP "headers already sent" warning.
e.g. A normal HTTP server->client response looks like this:
HTTP/1.1 200 OK
Content-type: text/html
Cookie: ...cookie data here ...
<html><body>Hi mom!</body></html>
But if you do output first, BEFORE calling setcookie, you'd end up with something like this:
HTTP/1.1 200 OK
Content-type: text/html
<html><body>Hi mom!</body></html>
Cookie: ... cookie data here ...
which doesn't work. Headers are only headers when they're in the header block of the response. If they show up in the body, they're not a header - they're part of the content. That's why PHP issues the "headers already sent", and doesn't send the cookie. It can't - the train has already left the station.
No. setcookie() passes a Set-Cookie HTTP header to the webserver, which in turn transfers it to the client.
Any warning pertaining to the header is an issue of your code structure. See also: How to fix "Headers already sent" error in PHP
The result code of setcookie() does not indicate if the client honors the Set-Cookie HTTP header however. Because neither PHP nor the webserver knows.
Quite correct, the server cannot detect the clients settings, you should verify this with your own implementation.
If you send a cookie upon first request, checking if it exists on the next request. Then you would know if the client excepts cookies or not. When that isn't the case you only have the IP and browser headers to tell you it could be the same user.
The next request can either be a new page request or for example an Ajax request that also sends headers and thus including cookies (if the browser excepts this).
If you attach a unique identifier to the new request uri and get an empty cookie response from him or her, you know the browser does not except cookies.

Guzzle response with content-encoding: gzip comes back with incorrect content-length header

I have a symfony2 application where I am using the Guzzle http client to send a GET request to a server in order to retrieve the contents of a json file. The Guzzle response gets transformed into a Symfony2 response to the browser.
The Guzzle response comes back with the following headers:
Content-Encoding: gzip
Content-Length: 2255
Content-Type: application/json
When outputting the data to the UI/browser I notice that it gets cut off because the Content-Length is incorrect. The size of the file is closer to 4905 bytes, not 2255. 2255 is the exact length of the data up to the cut-off point. I suspect that the 2255 is the size of the gzipped data and it gets uncompressed at some point without updating the content-length. Now I did verify that I get all of the data back, however the content-length header is honored which is why the data gets cut off when I forward it to the browser. Interestingly, hitting the url to the json file directly yields the full contents even though the content-length is 2255 which means it gets ignored by Chrome when hitting the file directly. Same if I use the POSTman REST client to make the GET request - full contents get displayed.
By default, Guzzle has a request option decode_content = true for how the responses should be handled. I set it to false when submitting the request but that didn't seem to resolve the issue.
Before converting the Guzzle response to a Symfony response I removed the content-length header and that seems to solve the problem however I am not sure that's the best approach since RFC protocol states that a content-length header should be present unless a transfer-encoding header is present, which it isn't. https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
Another alternative is, since this is a streamed response, to get the size of the stream and correct the content-length, however the Guzzle implementation uses strlen() for this which has the undesirable affect of reading the whole stream.
What possible issues might I run into if I choose to omit the content-length header? And alternatively, is there a way to get the TRUE length of the contents without reading the whole stream and simply update the content-length header with the correct amount?

Why use strtolower to verify $_SERVER['HTTP_X_REQUESTED_WITH']?

It's extremely common to see code like this for checking XHR:
if (
isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'
) {
// ajax happened
}
Why is strtolower always used here instead of just comparing to XMLHttpRequest? Is it just paranoia or is there a real reason?
HTTP headers in the request are always included by the client. A client can be a shell script, a browser or an Ajax call fired by Javascript or a Javascript framework.
The "j" in Ajax stands for Javascript, but the HTTP request of thus can be created with any other script or program.
From Common non-standard request headers:
X-Requested-With: mainly used to identify Ajax requests. Most JavaScript frameworks send this header with value of XMLHttpRequest XMLHttpRequest.
All headers starting with X-are non standard headers, which means there is no official document defining their values.
From https://www.rfc-editor.org/rfc/rfc6648:
Historically, designers and implementers of application protocols
have often distinguished between standardized and unstandardized
parameters by prefixing the names of unstandardized parameters with
the string "X-" or similar constructs.
The HTTP protocol is not fixed or static, there are just standards you should follow.
You can even create your own HTTP request and put individual headers in, for example:
X-Requested-With: MySuperCURLScript
X-MyOwnHeader: Cool!
To answer your question, if one client sends the header X-Requested-With: XMLHttpRequest, the other sends X-Requested-With: xmlHttpRequest, you can simply lowercase 'em all and be sure not to miss different notations. Simple as that!
Security aspect: The headers and its contents are only information, not something you should 100% rely on. If you have a deep look into CURL, or Google results for "send http header with php", you'll quickly find out that any information can be sent and faked easily. HTTP headers have often been abused to hack servers, by sending manipulated cookies (- a cookie is a header, simple as that: Cookie: ...), manipulated file information (upload a fake gif which is an exe file and so on), manipulating session data, POST / request data.

Send HTTP headers before or after a cookie header?

I was wondering if there are any problems or difference between sending normal headers before or after sending cookie headers. Do some browsers prefer a certain order to headers? If the cookie header is to large would subsequent headers never be parsed?
setcookie("TestCookie", $value);
header("Content-type: text/javascript");
or
header('Location: http://www.example.com/');
setcookie("TestCookie", $value);
or
setcookie("SuperLargeCookie", $massive_value);
setcookie("TinyCookie", $small_value);
header("Status: 404 Not Found");
There is no difference. The Http protocol does not specify that headers are to be in a certain order. Browsers do not differentiate based on the order of headers either.
The total length of Http headers does have a limit. This limit is imposed by the server and not the browser. Typically between 8K and 16K. However this is configurable.
It really doesn't matter as long as the other HTTP headers have not been sent. setcookie() actually writes a header itself:
Set-Cookie: SuperLargeCookie=whatever; Max-Age=3600; Version=1
similar to a header() call:
Location: http://www.example.com/redirect
HTTP messages span packets all the time, so you'd be hard-pressed to overfill one unless you're jamming tons of kilobytes in there. If you need to do that, consider a better design. Browsers don't care about the order of headers since different servers (and applications) append headers all the time. Cookies are implemented as HTTP headers, so they should appear like so in the HTTP request:
Cookie: TestCookie=value\r\n
Content-type: text/javascript\r\n
\r\n
I'm not sure what the Status header is supposed to do in your example, but I don't think it's right since the webserver will set a 200 OK response code if the code executes correctly... The header function page has this examaple:
<?php
header("HTTP/1.0 404 Not Found");
?>
With the PHP header function, just make sure you're not writing any text out before issuing it. Otherwise, you could mess everything up.

Categories