laminas/laminas-diactoros

Environment variables prefixed with `CONTENT_` may overwrite HTTP-Headers provided by the client

bcremer opened this issue · 3 comments

Bug Report

Q A
Version(s) all

Summary

Environment variables prefixed with CONTENT_ may overwrite HTTP-Headers provided by the client

Current behavior

This may be entirely expected behavior but I have not found a spec or warning in the documentation.
It's a least surprising to find unrelated environment variables as request header and potentially a security problem when comparing the env-variable against the header during authentication.

Environment variables prefixed with CONTENT_ will be populated as http-headers:

I stumbled about this behavior in a project with the following .env-file:

CONTENT_API_PASSWORD=password-for-content-api

The .env file is loaded into the $_SERVER superglobal via Dotenv\Dotenv::createImmutable().

The request object is created via \Laminas\Diactoros\ServerRequestFactory::fromGlobals.
The request object now contains content-api-password as request-header $request->getHeader('content-api-password').

Environment variables prefixed with CONTENT_ max overwrite client provided HTTP-Headers:

If the same header name is provided by an http-client the $_SERVER superglobal contains a HTTP_CONTENT_API_PASSWORD key.

In the current implementation the order is relevant. Whatever key is defined later in $_SERVER will be used to populate the HTTP-Header.

How to reproduce

        $server = [
            'HTTP_CONTENT_API_PASSWORD' => 'password_from_header',
            'CONTENT_API_PASSWORD' => 'password_from_env',
        ];

        $request = ServerRequestFactory::fromGlobals($server);

        $this->assertSame('password_from_env', $request->getHeader('content-api-password')[0]);


        $server = [
            'CONTENT_API_PASSWORD' => 'password_from_env',
            'HTTP_CONTENT_API_PASSWORD' => 'password_from_header',
        ];

        $request = ServerRequestFactory::fromGlobals($server);

        $this->assertSame('password_from_header', $request->getHeader('content-api-password')[0]);

Expected behavior

        $server = [
            'HTTP_CONTENT_API_PASSWORD' => 'password_from_header',
            'CONTENT_API_PASSWORD' => 'password_from_env',
        ];

        $request = ServerRequestFactory::fromGlobals($server);

        $this->assertSame('password_from_header', $request->getHeader('content-api-password')[0]);


        $server = [
            'CONTENT_API_PASSWORD' => 'password_from_env',
            'HTTP_CONTENT_API_PASSWORD' => 'password_from_header',
        ];

        $request = ServerRequestFactory::fromGlobals($server);

        $this->assertSame('password_from_header', $request->getHeader('content-api-password')[0]);

I agree we should make these changes.

Primary issue is... the MarshalHeadersFromSapiTest has a few test cases that fail if I make this change, as it was explicitly testing that things like CONTENT_FOO and CONTENT_BAZ in the $_SERVER array were being converted to headers. As such, this is potentially a BC break, as we'd be changing existing behavior.

I'm going to ask the TSC if the change can be done in next minor (2.6.0) as I'd call the current behavior incorrect, or if we absolutely need to do a new major (3.0.0).

One suggestion by @boesing is to potentially add a feature flag that is toggled via an ENV variable, and have it default to existing behavior when not present. This would allow you to opt-in to the new behavior, while keeping it backwards compatible by default. Next major version would remove the bad behavior of the current version.

I'll start work on that shortly.