CSP: Allow an option of disabling nonces
[lhc/web/wiklou.git] / tests / phpunit / includes / ContentSecurityPolicyTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 class ContentSecurityPolicyTest extends MediaWikiTestCase {
6 /** @var ContentSecurityPolicy */
7 private $csp;
8
9 protected function setUp() {
10 global $wgUploadDirectory;
11 $this->setMwGlobals( [
12 'wgAllowExternalImages' => false,
13 'wgAllowExternalImagesFrom' => [],
14 'wgAllowImageTag' => false,
15 'wgEnableImageWhitelist' => false,
16 'wgCrossSiteAJAXdomains' => [
17 'sister-site.somewhere.com',
18 '*.wikipedia.org',
19 '??.wikinews.org'
20 ],
21 'wgScriptPath' => '/w',
22 'wgForeignFileRepos' => [ [
23 'class' => ForeignAPIRepo::class,
24 'name' => 'wikimediacommons',
25 'apibase' => 'https://commons.wikimedia.org/w/api.php',
26 'url' => 'https://upload.wikimedia.org/wikipedia/commons',
27 'thumbUrl' => 'https://upload.wikimedia.org/wikipedia/commons/thumb',
28 'hashLevels' => 2,
29 'transformVia404' => true,
30 'fetchDescription' => true,
31 'descriptionCacheExpiry' => 43200,
32 'apiThumbCacheExpiry' => 0,
33 'directory' => $wgUploadDirectory,
34 'backend' => 'wikimediacommons-backend',
35 ] ],
36 ] );
37 // Note, there are some obscure globals which
38 // could affect the results which aren't included above.
39
40 RepoGroup::destroySingleton();
41 $context = RequestContext::getMain();
42 $resp = $context->getRequest()->response();
43 $conf = $context->getConfig();
44 $csp = new ContentSecurityPolicy( 'secret', $resp, $conf );
45 $this->csp = TestingAccessWrapper::newFromObject( $csp );
46
47 return parent::setUp();
48 }
49
50 /**
51 * @dataProvider providerFalsePositiveBrowser
52 * @covers ContentSecurityPolicy::falsePositiveBrowser
53 */
54 public function testFalsePositiveBrowser( $ua, $expected ) {
55 $actual = ContentSecurityPolicy::falsePositiveBrowser( $ua );
56 $this->assertEquals( $expected, $actual, $ua );
57 }
58
59 public function providerFalsePositiveBrowser() {
60 // @codingStandardsIgnoreStart Generic.Files.LineLength
61 return [
62 [ 'Mozilla/5.0 (X11; Linux i686; rv:41.0) Gecko/20100101 Firefox/41.0', true ],
63 [ 'Mozilla/5.0 (X11; U; Linux i686; en-ca) AppleWebKit/531.2+ (KHTML, like Gecko) Version/5.0 Safari/531.2+ Debian/squeeze (2.30.6-1) Epiphany/2.30.6', false ]
64 ];
65 // @codingStandardsIgnoreEnd Generic.Files.LineLength
66 }
67
68 /**
69 * @dataProvider providerMakeCSPDirectives
70 * @covers ContentSecurityPolicy::makeCSPDirectives
71 */
72 public function testMakeCSPDirectives(
73 $policy,
74 $expectedFull,
75 $expectedReport,
76 $expectedRestricted
77 ) {
78 $actualFull = $this->csp->makeCSPDirectives( $policy, ContentSecurityPolicy::FULL_MODE );
79 $actualReport = $this->csp->makeCSPDirectives(
80 $policy, ContentSecurityPolicy::REPORT_ONLY_MODE
81 );
82 $actualRestricted = $this->csp->makeCSPDirectives(
83 $policy, ContentSecurityPolicy::FULL_MODE_RESTRICTED
84 );
85 $policyJson = formatJson::encode( $policy );
86 $this->assertEquals( $expectedFull, $actualFull, "full: " . $policyJson );
87 $this->assertEquals( $expectedReport, $actualReport, "report: " . $policyJson );
88 $this->assertEquals( $expectedRestricted, $actualRestricted, "restricted: " . $policyJson );
89 }
90
91 public function providerMakeCSPDirectives() {
92 // @codingStandardsIgnoreStart Generic.Files.LineLength
93 return [
94 [ false, '', '', '' ],
95 [
96 [ 'useNonces' => false ],
97 "script-src 'unsafe-eval' 'self' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
98 "script-src 'unsafe-eval' 'self' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
99 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'"
100 ],
101 [
102 true,
103 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
104 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
105 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
106 ],
107 [
108 [],
109 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
110 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
111 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
112 ],
113 [
114 [ 'script-src' => [ 'http://example.com', 'http://something,else.com' ] ],
115 "script-src 'unsafe-eval' 'self' 'nonce-secret' http://example.com http://something%2Celse.com 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
116 "script-src 'unsafe-eval' 'self' 'nonce-secret' http://example.com http://something%2Celse.com 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
117 "script-src 'unsafe-eval' 'self' http://example.com http://something%2Celse.com sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
118 ],
119 [
120 [ 'unsafeFallback' => false ],
121 "script-src 'unsafe-eval' 'self' 'nonce-secret' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
122 "script-src 'unsafe-eval' 'self' 'nonce-secret' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
123 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
124 ],
125 [
126 [ 'unsafeFallback' => true ],
127 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
128 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
129 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
130 ],
131 [
132 [ 'default-src' => false ],
133 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
134 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
135 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
136 ],
137 [
138 [ 'default-src' => true ],
139 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
140 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
141 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'",
142 ],
143 [
144 [ 'default-src' => [ 'https://foo.com', 'http://bar.com', 'baz.de' ] ],
145 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
146 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
147 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'",
148 ],
149 [
150 [ 'includeCORS' => false ],
151 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline'; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
152 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline'; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
153 "script-src 'unsafe-eval' 'self'; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
154 ],
155 [
156 [ 'includeCORS' => false, 'default-src' => true ],
157 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline'; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
158 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline'; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
159 "script-src 'unsafe-eval' 'self'; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org 'unsafe-inline'",
160 ],
161 [
162 [ 'includeCORS' => true ],
163 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
164 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
165 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
166 ],
167 [
168 [ 'report-uri' => false ],
169 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
170 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
171 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
172 ],
173 [
174 [ 'report-uri' => true ],
175 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
176 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
177 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
178 ],
179 [
180 [ 'report-uri' => 'https://example.com/index.php?foo;report=csp' ],
181 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri https://example.com/index.php?foo%3Breport=csp",
182 "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri https://example.com/index.php?foo%3Breport=csp",
183 "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'",
184 ],
185 ];
186 }
187
188 /**
189 * @covers ContentSecurityPolicy::makeCSPDirectives
190 */
191 public function testMakeCSPDirectivesImage() {
192 global $wgAllowImageTag;
193 $origImg = wfSetVar( $wgAllowImageTag, true );
194
195 $actual = $this->csp->makeCSPDirectives( true, ContentSecurityPolicy::FULL_MODE );
196
197 $wgAllowImageTag = $origImg;
198
199 $expected = "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&";
200 $this->assertEquals( $expected, $actual );
201 }
202
203 /**
204 * @covers ContentSecurityPolicy::makeCSPDirectives
205 */
206 public function testMakeCSPDirectivesReportUri() {
207 $actual = $this->csp->makeCSPDirectives(
208 true,
209 ContentSecurityPolicy::REPORT_ONLY_MODE
210 );
211 $expected = "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&";
212 $this->assertEquals( $expected, $actual );
213 // @codingStandardsIgnoreEnd Generic.Files.LineLength
214 }
215
216 /**
217 * @covers ContentSecurityPolicy::getHeaderName
218 */
219 public function testGetHeaderName() {
220 $this->assertEquals(
221 $this->csp->getHeaderName( ContentSecurityPolicy::REPORT_ONLY_MODE ),
222 'Content-Security-Policy-Report-Only'
223 );
224 $this->assertEquals(
225 $this->csp->getHeaderName( ContentSecurityPolicy::FULL_MODE ),
226 'Content-Security-Policy'
227 );
228 }
229
230 /**
231 * @covers ContentSecurityPolicy::getReportUri
232 */
233 public function testGetReportUri() {
234 $full = $this->csp->getReportUri( ContentSecurityPolicy::FULL_MODE );
235 $fullExpected = '/w/api.php?action=cspreport&format=json&';
236 $this->assertEquals( $full, $fullExpected, 'normal report uri' );
237
238 $report = $this->csp->getReportUri( ContentSecurityPolicy::REPORT_ONLY_MODE );
239 $reportExpected = $fullExpected . 'reportonly=1&';
240 $this->assertEquals( $report, $reportExpected, 'report only' );
241
242 global $wgScriptPath;
243 $origPath = wfSetVar( $wgScriptPath, '/tl;dr/a,%20wiki' );
244 $esc = $this->csp->getReportUri( ContentSecurityPolicy::FULL_MODE );
245 $escExpected = '/tl%3Bdr/a%2C%20wiki/api.php?action=cspreport&format=json&';
246 $wgScriptPath = $origPath;
247 $this->assertEquals( $esc, $escExpected, 'test esc rules' );
248 }
249
250 /**
251 * @dataProvider providerPrepareUrlForCSP
252 * @covers ContentSecurityPolicy::prepareUrlForCSP
253 */
254 public function testPrepareUrlForCSP( $url, $expected ) {
255 $actual = $this->csp->prepareUrlForCSP( $url );
256 $this->assertEquals( $actual, $expected, $url );
257 }
258
259 public function providerPrepareUrlForCSP() {
260 global $wgServer;
261 return [
262 [ $wgServer, false ],
263 [ 'https://example.com', 'https://example.com' ],
264 [ 'https://example.com:200', 'https://example.com:200' ],
265 [ 'http://example.com', 'http://example.com' ],
266 [ 'example.com', 'example.com' ],
267 [ '*.example.com', '*.example.com' ],
268 [ 'https://*.example.com', 'https://*.example.com' ],
269 [ '//example.com', 'example.com' ],
270 [ 'https://example.com/path', 'https://example.com' ],
271 [ 'https://example.com/path:', 'https://example.com' ],
272 [ 'https://example.com/Wikipedia:NPOV', 'https://example.com' ],
273 [ 'https://tl;dr.com', 'https://tl%3Bdr.com' ],
274 [ 'yes,no.com', 'yes%2Cno.com' ],
275 [ '/relative-url', false ],
276 [ '/relativeUrl:withColon', false ],
277 [ 'data:', 'data:' ],
278 [ 'blob:', 'blob:' ],
279 ];
280 }
281
282 /**
283 * @covers ContentSecurityPolicy::escapeUrlForCSP
284 */
285 public function testEscapeUrlForCSP() {
286 $escaped = $this->csp->escapeUrlForCSP( ',;%2B' );
287 $this->assertEquals( $escaped, '%2C%3B%2B' );
288 }
289
290 /**
291 * @dataProvider providerCSPIsEnabled
292 * @covers ContentSecurityPolicy::isNonceRequired
293 */
294 public function testCSPIsEnabled( $main, $reportOnly, $expected ) {
295 global $wgCSPReportOnlyHeader, $wgCSPHeader;
296 global $wgCSPHeader;
297 $oldReport = wfSetVar( $wgCSPReportOnlyHeader, $reportOnly );
298 $oldMain = wfSetVar( $wgCSPHeader, $main );
299 $res = ContentSecurityPolicy::isNonceRequired( RequestContext::getMain()->getConfig() );
300 wfSetVar( $wgCSPReportOnlyHeader, $oldReport );
301 wfSetVar( $wgCSPHeader, $oldMain );
302 $this->assertEquals( $res, $expected );
303 }
304
305 public function providerCSPIsEnabled() {
306 return [
307 [ true, true, true ],
308 [ false, true, true ],
309 [ true, false, true ],
310 [ false, false, false ],
311 [ false, [], true ],
312 [ [], false, true ],
313 [ [ 'default-src' => [ 'foo.example.com' ] ], false, true ],
314 [ [ 'useNonces' => false ], [ 'useNonces' => false ], false ],
315 [ [ 'useNonces' => true ], [ 'useNonces' => false ], true ],
316 [ [ 'useNonces' => false ], [ 'useNonces' => true ], true ],
317 ];
318 }
319 }