• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / http / 29282633795

13 Jul 2026 08:30PM UTC coverage: 83.393% (+0.2%) from 83.198%
29282633795

push

github

dg
HttpExtension: default to trusting X-Forwarded-For only (BC break!)

Changes the configured default of `proxyHeaders` from "both" to "xForwarded",
so an application behind a proxy no longer trusts a client-supplied "Forwarded"
header by default (the common proxy sets X-Forwarded-For). The RequestFactory
PHP default is intentionally left at "both" for BC; this hardens only the
framework configuration.

BC: deployments whose proxy uses the "Forwarded" header must now set
`proxyHeaders: forwarded` (or `both`).

1 of 1 new or added line in 1 file covered. (100.0%)

25 existing lines in 2 files now uncovered.

939 of 1126 relevant lines covered (83.39%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.63
/src/Bridges/HttpDI/HttpExtension.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\Bridges\HttpDI;
9

10
use Nette;
11
use Nette\Schema\Expect;
12
use function is_array, strval;
13

14

15
/**
16
 * HTTP extension for Nette DI.
17
 *
18
 * @property object{
19
 *     proxy: array<string>,
20
 *     proxyHeaders: string,
21
 *     forceHttps: bool,
22
 *     headers: array<string, ?scalar>,
23
 *     frames: string|bool|null,
24
 *     csp: array<string, array<mixed>|scalar|null>,
25
 *     cspReportOnly: array<string, array<mixed>|scalar|null>,
26
 *     featurePolicy: array<string, array<mixed>|scalar|null>,
27
 *     cookiePath: ?string,
28
 *     cookieDomain: ?string,
29
 *     cookieSecure: bool|'auto'|null,
30
 *     disableNetteCookie: bool,
31
 * } $config
32
 */
33
class HttpExtension extends Nette\DI\CompilerExtension
34
{
35
        public function __construct(
1✔
36
                private readonly bool $cliMode = false,
37
        ) {
38
        }
1✔
39

40

41
        public function getConfigSchema(): Nette\Schema\Schema
42
        {
43
                return Expect::structure([
1✔
44
                        'proxy' => Expect::anyOf(Expect::arrayOf('string'), Expect::string()->castTo('array'))->firstIsDefault()->dynamic(),
1✔
45
                        'proxyHeaders' => Expect::anyOf('xForwarded', 'forwarded', 'both', 'none')->firstIsDefault(),
1✔
46
                        'forceHttps' => Expect::bool(false)->dynamic(),
1✔
47
                        'headers' => Expect::arrayOf('scalar|null')->default([
1✔
48
                                'X-Powered-By' => 'Nette Framework 3',
1✔
49
                                'Content-Type' => 'text/html; charset=utf-8',
50
                        ])->mergeDefaults(),
1✔
51
                        'frames' => Expect::anyOf(Expect::string(), Expect::bool(), null)->default('SAMEORIGIN'), // X-Frame-Options
1✔
52
                        'csp' => Expect::arrayOf('array|scalar|null'), // Content-Security-Policy
1✔
53
                        'cspReportOnly' => Expect::arrayOf('array|scalar|null'), // Content-Security-Policy-Report-Only
1✔
54
                        'featurePolicy' => Expect::arrayOf('array|scalar|null'), // Feature-Policy
1✔
55
                        'cookiePath' => Expect::string()->dynamic(),
1✔
56
                        'cookieDomain' => Expect::string()->dynamic(),
1✔
57
                        'cookieSecure' => Expect::anyOf('auto', null, true, false)->firstIsDefault()->dynamic(), // Whether the cookie is available only through HTTPS
1✔
58
                        'disableNetteCookie' => Expect::bool(false), // disables cookie use by Nette
1✔
59
                ]);
60
        }
61

62

63
        public function loadConfiguration(): void
64
        {
65
                $builder = $this->getContainerBuilder();
1✔
66
                $config = $this->config;
1✔
67

68
                $requestFactory = $builder->addDefinition($this->prefix('requestFactory'))
1✔
69
                        ->setFactory(Nette\Http\RequestFactory::class)
1✔
70
                        ->addSetup('setProxy', [
1✔
71
                                $config->proxy,
1✔
72
                                'forwarded' => $config->proxyHeaders === 'forwarded' || $config->proxyHeaders === 'both',
1✔
73
                                'xForwarded' => $config->proxyHeaders === 'xForwarded' || $config->proxyHeaders === 'both',
1✔
74
                        ]);
75

76
                if ($config->forceHttps) {
1✔
UNCOV
77
                        $requestFactory->addSetup('setForceHttps');
×
78
                }
79

80
                $request = $builder->addDefinition($this->prefix('request'))
1✔
81
                        ->setFactory('@Nette\Http\RequestFactory::fromGlobals');
1✔
82

83
                $response = $builder->addDefinition($this->prefix('response'))
1✔
84
                        ->setFactory(Nette\Http\Response::class);
1✔
85

86
                if ($config->cookiePath !== null) {
1✔
87
                        $response->addSetup('$cookiePath', [$config->cookiePath]);
1✔
88
                }
89

90
                if ($config->cookieDomain !== null) {
1✔
91
                        $value = $config->cookieDomain === 'domain'
1✔
92
                                ? $builder::literal('$this->getService(?)->getUrl()->getDomain(2)', [$request->getName()])
1✔
93
                                : $config->cookieDomain;
1✔
94
                        $response->addSetup('$cookieDomain', [$value]);
1✔
95
                }
96

97
                if ($config->cookieSecure !== null) {
1✔
98
                        $value = $config->cookieSecure === 'auto'
1✔
99
                                ? $builder::literal('$this->getService(?)->isSecured()', [$request->getName()])
1✔
100
                                : $config->cookieSecure;
1✔
101
                        $response->addSetup('$cookieSecure', [$value]);
1✔
102
                }
103

104
                if ($this->name === 'http') {
1✔
105
                        $builder->addAlias('nette.httpRequestFactory', $this->prefix('requestFactory'));
1✔
106
                        $builder->addAlias('httpRequest', $this->prefix('request'));
1✔
107
                        $builder->addAlias('httpResponse', $this->prefix('response'));
1✔
108
                }
109

110
                if (!$this->cliMode) {
1✔
111
                        $this->sendHeaders();
1✔
112
                }
113
        }
1✔
114

115

116
        private function sendHeaders(): void
117
        {
118
                $config = $this->config;
1✔
119
                $headers = array_map(strval(...), $config->headers);
1✔
120

121
                if (isset($config->frames) && $config->frames !== true && !isset($headers['X-Frame-Options'])) {
1✔
122
                        $frames = $config->frames;
1✔
123
                        if ($frames === false) {
1✔
UNCOV
124
                                $frames = 'DENY';
×
125
                        } elseif (preg_match('#^https?:#', $frames)) {
1✔
UNCOV
126
                                $frames = "ALLOW-FROM $frames";
×
127
                        }
128

129
                        $headers['X-Frame-Options'] = $frames;
1✔
130
                }
131

132
                foreach (['csp', 'cspReportOnly'] as $key) {
1✔
133
                        if (empty($config->$key)) {
1✔
134
                                continue;
1✔
135
                        }
136

UNCOV
137
                        $value = self::buildPolicy($config->$key);
×
UNCOV
138
                        if (str_contains($value, "'nonce'")) {
×
UNCOV
139
                                $this->initialization->addBody('$cspNonce = base64_encode(random_bytes(16));');
×
140
                                $value = Nette\DI\ContainerBuilder::literal(
×
UNCOV
141
                                        'str_replace(?, ? . $cspNonce, ?)',
×
UNCOV
142
                                        ["'nonce", "'nonce-", $value],
×
143
                                );
144
                        }
145

UNCOV
146
                        $headers['Content-Security-Policy' . ($key === 'csp' ? '' : '-Report-Only')] = $value;
×
147
                }
148

149
                if (!empty($config->featurePolicy)) {
1✔
UNCOV
150
                        $headers['Feature-Policy'] = self::buildPolicy($config->featurePolicy);
×
151
                }
152

153
                $this->initialization->addBody('$response = $this->getService(?);', [$this->prefix('response')]);
1✔
154
                foreach ($headers as $key => $value) {
1✔
155
                        if ($value !== '') {
1✔
156
                                $this->initialization->addBody('$response->setHeader(?, ?);', [$key, $value]);
1✔
157
                        }
158
                }
159

160
                if (!$config->disableNetteCookie) {
1✔
161
                        $this->initialization->addBody(
1✔
162
                                'Nette\Http\Helpers::initCookie($this->getService(?), $response);',
1✔
163
                                [$this->prefix('request')],
1✔
164
                        );
165
                }
166
        }
1✔
167

168

169
        /** @param array<string, array<mixed>|scalar|null>  $config */
170
        private static function buildPolicy(array $config): string
171
        {
UNCOV
172
                $nonQuoted = ['require-sri-for' => 1, 'sandbox' => 1];
×
173
                $value = '';
×
174
                foreach ($config as $type => $policy) {
×
175
                        if ($policy === false) {
×
176
                                continue;
×
177
                        }
178

UNCOV
179
                        $policy = $policy === true ? [] : (array) $policy;
×
180
                        $value .= $type;
×
181
                        foreach ($policy as $item) {
×
182
                                if (is_array($item)) {
×
UNCOV
183
                                        $item = key($item) . ':';
×
184
                                }
185

UNCOV
186
                                $value .= !isset($nonQuoted[$type]) && preg_match('#^[a-z-]+$#D', $item)
×
UNCOV
187
                                        ? " '$item'"
×
188
                                        : " $item";
×
189
                        }
190

UNCOV
191
                        $value .= '; ';
×
192
                }
193

UNCOV
194
                return $value;
×
195
        }
196
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc