ContentSecurityPolicyTest.php 16 KB

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