Skip to content

Added the documentation for the trusted_hosts option #3876

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

Closed
wants to merge 3 commits into from
Closed

Added the documentation for the trusted_hosts option #3876

wants to merge 3 commits into from

Conversation

javiereguiluz
Copy link
Member

Q A
Doc fix? no
New docs? yes
Applies to 2.3+
Fixed tickets #3586

@@ -114,6 +115,69 @@ services related to testing your application (e.g. ``test.client``) are loaded.
This setting should be present in your ``test`` environment (usually via
``app/config/config_test.yml``). For more information, see :doc:`/book/testing`.

.. _reference-framework-trusted-hosts:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you add this? Did you want to add a link to this section, but you forgot to commit that link?


The value of the ``$_SERVER['HOST']`` parameter cannot be safely trusted because
users can manipulate it. This option whitelists the hosts that your Symfony
application can respond to.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "can respond to"? It sounds like the app will not work unless you put your domain name here. I don't think this is what we mean - I actually don't know what this feature does tbh :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To understand the feature, it's better to see the code:

1. When booting, FrameworkBundle sets the trusted hosts of the Request:

class FrameworkBundle extends Bundle
{
    public function boot()
    {
        // ...

        if ($trustedHosts = $this->container->getParameter('kernel.trusted_hosts')) {
            Request::setTrustedHosts($trustedHosts);
        }
    }

    // ...
}

2. The Request uses this configuration value to define the trusted hosts patterns:

    public static function setTrustedHosts(array $hostPatterns)
    {
        self::$trustedHostPatterns = array_map(function ($hostPattern) {
            return sprintf('{%s}i', str_replace('}', '\\}', $hostPattern));
        }, $hostPatterns);
        // we need to reset trusted hosts on trusted host patterns change
        self::$trustedHosts = array();
    }

3. When dealing with the Request, one of the called methods is prepareRequestUri():

    protected function prepareRequestUri()
    {
        $requestUri = '';

        if ($this->headers->has('X_ORIGINAL_URL')) {
            // ...
        } elseif ($this->headers->has('X_REWRITE_URL')) {
            // ...
        } elseif ($this->server->get('IIS_WasUrlRewritten') == '1' && $this->server->get('UNENCODED_URL') != '') {
            // ...
        } elseif ($this->server->has('REQUEST_URI')) {
            $requestUri = $this->server->get('REQUEST_URI');
            // HTTP proxy reqs setup request URI with scheme and host
            // [and port] + the URL path, only use URL path
            $schemeAndHttpHost = $this->getSchemeAndHttpHost();
            if (strpos($requestUri, $schemeAndHttpHost) === 0) {
                $requestUri = substr($requestUri, strlen($schemeAndHttpHost));
            }
        } elseif ($this->server->has('ORIG_PATH_INFO')) {
            // ...
        }

        // normalize the request URI to ease creating sub-requests from this request
        $this->server->set('REQUEST_URI', $requestUri);

        return $requestUri;
    }

4. Indirectly, the getSchemeAndHttpHost() method executes the getHost() method, which will throw an exception if the host of the user request doesn't match the trusted host patterns:

    public function getHost()
    {
        // ...

        // as the host can come from the user (HTTP_HOST and depending on the
        // configuration, SERVER_NAME too can come from the user)
        // check that it does not contain forbidden characters (see RFC 952 and RFC 2181)
        if ($host && !preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host)) {
            throw new \UnexpectedValueException(sprintf('Invalid Host "%s"', $host));
        }

        if (count(self::$trustedHostPatterns) > 0) {
            // to avoid host header injection attacks, you should provide a
            // list of trusted host patterns

            if (in_array($host, self::$trustedHosts)) {
                return $host;
            }

            foreach (self::$trustedHostPatterns as $pattern) {
                if (preg_match($pattern, $host)) {
                    self::$trustedHosts[] = $host;

                    return $host;
                }
            }

            throw new \UnexpectedValueException(sprintf('Untrusted Host "%s"', $host));
        }

        return $host;
    }

So, if you leave this option blank, everything works as previously did. If you set any value, the application will thro an exception if the host provided by the user request doesn't match the configured regular expressions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so the purpose of the feature is so that I can "restrict my application to respond to only a sub-set of hosts". Is that accurate? What exactly is the security risk? Or said differently, why would a developer care about this?

I would like to re-word the first few sentences to address the use-case. This is totally invented (since I don't know the real use-case), but for example:

If you use the HTTP host for security (e.g., you can only access certain URLs from a specific host), then you should explicitly make sure your application only works for the host names that you're expecting.

That's weak wording... but you get the idea. I'm purposefully being "dumb" (and not looking up more detailson) so that if I understand this feature, then we're good :).

Thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ricardclau
Copy link
Contributor

I would add a note in @javiereguiluz ´s explanation about the scenario where you run a Symfony app in a cloud environment where you can trust the ELB on top of your servers (provided there is no direct access to them) and can be sorted using this piece of code:

Request::setTrustedProxies(array($request->server->get('REMOTE_ADDR')));

I added a similar comment here #2946 (comment)

This seems to be a recurrent issue for people who start using Symfony since they don´t know how to make methods like $request->isSecure() work

@javiereguiluz
Copy link
Member Author

@ricardclau maybe you can also shed some light to this issue: symfony/symfony#11583

@weaverryan
Copy link
Member

@javiereguiluz I have 2 quick questions left on this:

  1. What is the default behavior? Meaning, if I don't have a trusted_hosts option specified? I assume it means "allow all hosts?"

  2. If I have trusted_hosts and a request comes into my site with a HOST header that does not match, what happens? Does the application not run entirely? And if so, does the user get a 404?

It's looking really good! It's nice that I haven't looked at the code for this yet - I can read it as a user and see what I don't understand :).

Thanks!

@javiereguiluz
Copy link
Member Author

@ricardclau I have no real experience with those cloud scenarios, so I really cannot add the note that you are suggesting :( Could you please prepare that note so I can add it to this explanation? Thanks!

@weaverryan

1. The default behavior is explained at the end:

The default value for this option is an empty array, meaning that the application
can respond to any given host.

2. What will happen if you set trusted_hosts and receive a HOST header that doesn't match?

The code executed in that case will be https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/Request.php#L1177-1193, which will throw a UnexpectedValueException and the user will see it as an error page.

@stof
Copy link
Member

stof commented Aug 7, 2014

@javiereguiluz there is a small issue though: the client will receive a 500 error in this case, while a 400 might be better.

@ricardclau
Copy link
Contributor

@javiereguiluz I misread the whole thing, I was talking about trusted_proxies, no trusted_hosts, sorry about the confusion. A cloud environment affects the proxies but the hosts stay the same. Sorry again!

The Symfony Request::getHost() method might be vulnerable to some of these attacks
because it depends on the configuration of your web server. One simple solution
to avoid these attacks is to whitelist the hosts that your Symfony application
can respond to. That's the purpose of this ``trusted_hosts`` option:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add one more sentence at the end. Something like this:

If the incoming request's hostname doesn't match one in this list, the
application won't respond and the user will receive a 500 response:

I wasn't sure initially if maybe if your host wasn't in this list that Symfony still responded but used a different absolute URL (or something like that). This is clear that literally your app won't work at all.

@weaverryan
Copy link
Member

@javiereguiluz I added one more small request - thanks for your patience :). It will be very clear how this works now (even to me!).

wouterj added a commit to wouterj/symfony-docs that referenced this pull request Aug 25, 2014
@wouterj
Copy link
Member

wouterj commented Aug 25, 2014

I copied this PR into #4121 (unfortunately, because you've removed this branch on your repo, I couldn't give you credits for it).

@wouterj wouterj closed this Aug 25, 2014
@stof
Copy link
Member

stof commented Aug 25, 2014

@wouterj you can fetch the code of any PR on a repo, even if the source repo was removed. github uses references in a separate namespace than branches to store the code of PRs:

$ git fetch https://github.com/symfony/symfony-docs.git refs/pull/3878
$ git checkout FETCH_HEAD

For open PRs which are mergeable (i.e. no conflict), it is even possible to fetch refs/pull/3878/merge. The head of such ref will be a merge commit generated automatically (this is what Travis is using for instance)

@wouterj
Copy link
Member

wouterj commented Aug 25, 2014

wow, thanks @stof. I didn't knew that :)

Btw, I always use hub, so I this case I did git checkout https://github.com/symfony/symfony-docs/pull/3876 which gave me an error. It's strange even github's own tools didn't use refs/pull/3878...

@stof
Copy link
Member

stof commented Aug 25, 2014

@wouterj hub was probably written before this existed in Github in a public way. Thus, given it creates a branch tracking the upstream one, it might make sense to do it the way they do.
I know that github for windows relies on the PR references (maybe github for mac too, but I never tried it).

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.

5 participants