2

With Content-Security-Policy headers there is often a need to send more than one such header or to union merge these headers before sending them. This arises from the fact that each module/package of an application may define its own CSP.

Right now ZF3 doesn't seem to have a way to handle such a scenario. If I try to add multple CSP headers, they keep overwriting each other so that only the last added header is sent.

Code to reproduce the issue

$headers = $controller->getResponse()->getHeaders();
$headers->addHeader(new ContentSecurityPolicy($someDirectives));
$headers->addHeader(new ContentSecurityPolicy($someOtherDirectives));

Expected results

The expected result is a response with two CSP headers (OR a union merged CSP).

Actual results

The second addition overwrites the first, the response only contains that one CSP.

Question

How can I make ZF3 send multple headers with the same fieldname?


For more information about this problem, also see my own issue on github https://github.com/zendframework/zend-http/issues/159

markus
  • 40,136
  • 23
  • 97
  • 142
  • 1
    Have you had a look at [this answer](https://stackoverflow.com/a/3097052/1155833) about multiple values for a single header? If yes, might that not also apply here? Else, might it be that `ContentSecurityPolicy` needs to implement `MultipleHeaderInterface` instead of `HeaderInterface` ? (spitballing, interesting question, +1) – rkeet Oct 24 '18 at 06:49
  • @rkeet Yeah, good points, I'm aware of these and I also think/thought that `ContentSecurityPolicy` should indeed implement `MultipleHeaderInterface` but I already tried making it work like that and `zend-http` just gave me new grief. See also comments on the linked github issue. – markus Oct 24 '18 at 09:27

3 Answers3

2

You should be able to create a simple workaround using GenericMultipleHeader as a reference (and changing comma delimiter to semicolon):

class MultiContentSecurityPolicy extends ContentSecurityPolicy implements MultipleHeaderInterface {

    public static function fromString($headerLine)
    {
        list($fieldName, $fieldValue) = GenericHeader::splitHeaderLine($headerLine);
        if (strpos($fieldValue, ';')) {
            $headers = [];
            foreach (explode(';', $fieldValue) as $multiValue) {
                $headers[] = new static($fieldName, $multiValue);
            }
            return $headers;
        } else {
            $header = new static($fieldName, $fieldValue);
            return $header;
        }
    }

    public function toStringMultipleHeaders(array $headers)
    {
        $name  = $this->getFieldName();
        $values = [$this->getFieldValue()];
        foreach ($headers as $header) {
            if (! $header instanceof static) {
                throw new Exception\InvalidArgumentException(
                    'This method toStringMultipleHeaders was expecting an array of headers of the same type'
                );
            }
            $values[] = $header->getFieldValue();
        }
        return $name . ': ' . implode(';', $values) . "\r\n";
    }

}

Then use that class instead of ContentSecurityPolicy:

$headers = $controller->getResponse()->getHeaders();
$headers->addHeader(new MultiContentSecurityPolicy($someDirectives));
$headers->addHeader(new MultiContentSecurityPolicy($someOtherDirectives));

Since Zend checks the interface rather than the class, should work fine.

Jim
  • 3,210
  • 2
  • 17
  • 23
1

This is the accepted HTTP standard and the PHP Core upholds this. http://php.net/manual/en/function.header.php

If you set headers in PHP header("TESTHeader: Test1"); header("TESTHeader: Test2") only one will come through and this is correct to specification RFC2616 Section 4.2 Page 31&32

If you wish to send multiple values your header should construct as header("TESTHeader: Test1, Test2");. while it is possible to send multiple same name headers through PHP it is not recommended as browsers & servers receiving 2 sets of the same header should convert them to the above style this could cause problems as you will not know for certain what format they are in. header("TESTHeader: Test1", false); header("TESTHeader: Test2", false). depending on the server or clients adherence to the RFC or HTTP Version.

So this answer is the reason as to why you are not allowed to send the same header multiple times in ZF3, it can't identify when to use the overwrite or not to based on you setting the header. to get around this and use multi-valued headers you can use Jim's answer

Community
  • 1
  • 1
Barkermn01
  • 6,781
  • 33
  • 83
  • From the RFC you're linking: "Multiple message-header fields with the same field-name MAY be present in a message if and only if the entire field-value for that header field is defined as a comma-separated list [i.e., #(values)]. It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, without changing the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma." – markus Nov 05 '18 at 11:43
  • Ergo if PHP really implements it like this (source?) then PHP implements it wrongly. It is the client's task to to the union merge. – markus Nov 05 '18 at 11:44
  • Ok you missed so much of that, you quoted one snippet without reading the end of the same paragraph, "by appending each subsequent field-value to the first, each separated by a comma" this RFC is not for the client alone it's more for the server to be compatible with clients – Barkermn01 Nov 05 '18 at 12:44
  • I Also Said that PHP Upholds the RFC, and show how "your header should construct" notice the word should not "forced to" or "you have to". if you had read the PHP docs for the header function http://php.net/manual/en/function.header.php you would see it had a `replace` argument I'm so sorry I assumed you would read the official documentation for the function!!! – Barkermn01 Nov 05 '18 at 12:47
  • No need for this type of comments. You're right in your comments but your answer is not worded correctly. If the RFC allows multiple headers with the same name ("MAY") and PHP also provides the possibility to set `replace` to `false` this means that PHP indeed upholds the RFC as you said albeit not by letting only one go through, but by providing the `replace` option. – markus Nov 05 '18 at 17:45
  • Therefore the second paragraph of your answers could be something like "If you wish to send multiple headers with the same name, set replace to false." The third paragraph is then not the right conclusion because if ZF3 properly implemented the replace option, dublicate names would still be possible. Thanks for pointing that out to me, I'm going to look at the ZF implementation again to see how/if the replace option is used. – markus Nov 05 '18 at 17:45
  • No, it should not be you should never teach someone to use incorrect/backward supported methods! becasue your just teaching them methods that could be removed in later versions – Barkermn01 Nov 05 '18 at 17:46
  • In the context of the question it is a need/requirement directly resulting from the CSP concept which is very current. It will happen more and more often that several dependencies of a project will add their own CSP headers and it's not reasonable to expect them all to look for pre-existing CSP headers and merge their own directives. It is much more reasonable to expect languages and frameworks to allow/provide for sending multiple CSPs which will then be unified by another layer, e.g. the client, which is perfectly equipped to do that. – markus Nov 05 '18 at 17:53
  • Have you checked that does the CSP RFC state it's allowed to handle multiple headers and not multiple values because most applications that use headers don't support Multiple headers they merge them as per the RFC! because if they support multiple headers that are not merged they are in breach of the RFC "by appending each subsequent field-value to the first, each separated by a comma" so if you do it at server side the client side is still going to only get one header in its headers data store that has been merged. – Barkermn01 Nov 05 '18 at 17:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/183138/discussion-between-martin-barker-and-markus). – Barkermn01 Nov 05 '18 at 17:58
  • Just to finish this up as it currently stands, PHP is very rarely using its own WebServer (although it does have one), what in current practice happens is PHP gives the headers to a WebServer, the Web Server Sends them to the client, both the WebServer and the Client should convert them to the new standard (`comma, delimited`) if they were sent in the old way, this is made worse by Load Balancing or Proxy servers so you should use the new way from the start and stop the WebServer or Client from doing the conversion, and you get an unexpected result so Zend Framework is enforcing this. – Barkermn01 Oct 12 '21 at 10:55
0

make your own multipleheader class, add the function you need (MultipleHeaderInterface) then add your header in a multistering and finally call it in your

$headers = $controller->getResponse()->getHeaders();

(call the new function with the new fromStringHeaders)

Moubarak Hayal
  • 191
  • 1
  • 7