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