@@ -14,6 +14,12 @@ const self = `'self'`;
1414const unsafeInline = `'unsafe-inline'` ;
1515const wasmUnsafeEval = `'wasm-unsafe-eval'` ;
1616
17+ // Central has notifications defined in https://github.com/getodk/central/tree/master/docs, and served from GitHub Pages. These include:
18+ //
19+ // * https://getodk.github.io/central/news.html
20+ // * https://getodk.github.io/central/outdated-version.html
21+ const centralNotifications = 'https://getodk.github.io/central/' ;
22+
1723const asArray = val => {
1824 if ( val == null ) return [ ] ;
1925 if ( Array . isArray ( val ) ) return val ;
@@ -37,10 +43,14 @@ const allowGoogleTranslate = ({ 'connect-src':connectSrc, 'img-src':imgSrc, ...o
3743const contentSecurityPolicies = {
3844 'backend-unmodified' : {
3945 block : {
40- 'default-src' : 'NOTE:FROM-BACKEND:block' ,
46+ 'default-src' : 'NOTE:FROM-BACKEND:block' ,
47+ 'form-action' : 'NOTE:FROM-BACKEND:block' ,
48+ 'frame-ancestors' : 'NOTE:FROM-BACKEND:block' ,
4149 } ,
4250 reportOnly : {
43- 'default-src' : 'NOTE:FROM-BACKEND:reportOnly' ,
51+ 'default-src' : 'NOTE:FROM-BACKEND:reportOnly' ,
52+ 'form-action' : 'NOTE:FROM-BACKEND:reportOnly' ,
53+ 'frame-ancestors' : 'NOTE:FROM-BACKEND:reportOnly' ,
4454 } ,
4555 } ,
4656 'blank-html' : {
@@ -49,6 +59,8 @@ const contentSecurityPolicies = {
4959 reportSample ,
5060 none ,
5161 ] ,
62+ 'form-action' : none ,
63+ 'frame-ancestors' : self ,
5264 'img-src' : self , // allow favicon.ico
5365 'report-uri' : '/csp-report' ,
5466 } ) ,
@@ -63,15 +75,17 @@ const contentSecurityPolicies = {
6375 self ,
6476 ] ,
6577 'font-src' : self ,
78+ 'form-action' : self ,
79+ 'frame-ancestors' : none ,
6680 'frame-src' : [
6781 self ,
68- 'https://getodk.github.io/central/news.html' ,
82+ centralNotifications ,
6983 ] ,
7084 'img-src' : [
7185 'data:' ,
7286 'https:' ,
7387 ] ,
74- 'manifest-src' : none ,
88+ 'manifest-src' : self ,
7589 'media-src' : none ,
7690 'object-src' : none ,
7791 'script-src' : [
@@ -92,19 +106,25 @@ const contentSecurityPolicies = {
92106 } ,
93107 'disallow-all' : {
94108 block : {
95- 'default-src' : 'NOTE:FROM-BACKEND:block' ,
109+ 'default-src' : 'NOTE:FROM-BACKEND:block' ,
110+ 'form-action' : 'NOTE:FROM-BACKEND:block' ,
111+ 'frame-ancestors' : 'NOTE:FROM-BACKEND:block' ,
96112 } ,
97113 reportOnly : {
98114 'default-src' : [
99115 reportSample ,
100116 none ,
101117 ] ,
118+ 'form-action' : none ,
119+ 'frame-ancestors' : none ,
102120 'report-uri' : '/csp-report' ,
103121 } ,
104122 } ,
105123 enketo : {
106124 block : {
107- 'default-src' : 'NOTE:FROM-BACKEND:block' ,
125+ 'default-src' : 'NOTE:FROM-BACKEND:block' ,
126+ 'form-action' : 'NOTE:FROM-BACKEND:block' ,
127+ 'frame-ancestors' : 'NOTE:FROM-BACKEND:block' ,
108128 } ,
109129 reportOnly : allowGoogleTranslate ( {
110130 'default-src' : [
@@ -124,6 +144,8 @@ const contentSecurityPolicies = {
124144 self ,
125145 'https://fonts.gstatic.com/' ,
126146 ] ,
147+ 'form-action' : self ,
148+ 'frame-ancestors' : self ,
127149 'frame-src' : none ,
128150 'img-src' : [
129151 'data:' ,
@@ -173,14 +195,21 @@ const contentSecurityPolicies = {
173195 self ,
174196 'data:' ,
175197 ] ,
176- 'frame-src' : self , // web-forms pages also host /enketo-passthrough/ URLs via iframes
198+ 'form-action' : self ,
199+ 'frame-ancestors' : self ,
200+ 'frame-src' : [
201+ self , // web-forms pages also host /enketo-passthrough/ URLs via iframes
202+ centralNotifications ,
203+ ] ,
177204 'img-src' : [
178205 'blob:' ,
179206 'data:' ,
180207 'https:' ,
181208 ] ,
182- 'manifest-src' : none ,
183- 'media-src' : none ,
209+ 'manifest-src' : self ,
210+ 'media-src' : [
211+ 'blob:' ,
212+ ] ,
184213 'object-src' : none ,
185214 'script-src' : [
186215 reportSample ,
@@ -201,6 +230,12 @@ const contentSecurityPolicies = {
201230} ;
202231
203232describe ( 'Content-Security-Policy definitions' , ( ) => {
233+ const requiredDirectives = [
234+ 'default-src' ,
235+ 'form-action' ,
236+ 'frame-ancestors' ,
237+ ] ;
238+
204239 const supportsReportSample = [
205240 'default-src' ,
206241 'require-trusted-types-for' ,
@@ -224,7 +259,15 @@ describe('Content-Security-Policy definitions', () => {
224259 const policy = policies [ headerType ] ;
225260 if ( ! policy ) continue ;
226261
262+ it ( `should have required directives: ${ requiredDirectives } ` , ( ) => {
263+ assert . containsAllKeys ( policy , requiredDirectives ) ;
264+ } ) ;
265+
227266 describe ( `header: ${ headerNames [ headerType ] } ` , ( ) => {
267+ it ( `should have required directives: ${ requiredDirectives } ` , ( ) => {
268+ assert . containsAllKeys ( policy , requiredDirectives ) ;
269+ } ) ;
270+
228271 Object . entries ( policy )
229272 . map ( ( [ key , directive ] ) => [ key , asArray ( directive ) ] )
230273 . filter ( ( [ key , directive ] ) => ! ( directive . length === 1 && directive [ 0 ] === `NOTE:FROM-BACKEND:${ headerType } ` ) ) // eslint-disable-line no-unused-vars
@@ -441,17 +484,24 @@ function standardTestSuite({ fetchHttp, fetchHttp6, apiFetch, apiFetch6, forward
441484 } ) ;
442485
443486 [
444- [ '/index.html' , / < d i v i d = " a p p " > < \/ d i v > / ] ,
445- [ '/version.txt' , / ^ v e r s i o n s : / ] ,
446- [ '/favicon.ico' , / ^ \n $ / ] ,
447- ] . forEach ( ( [ path , expectedContent ] ) => {
487+ [ '/index.html' , 'text/html' , / < d i v i d = " a p p " > < \/ d i v > / ] ,
488+ [ '/version.txt' , 'text/plain' , / ^ v e r s i o n s : / ] ,
489+ [ '/android-chrome-192x192.png' , 'image/png' , / ^ \n $ / ] ,
490+ [ '/android-chrome-512x512.png' , 'image/png' , / ^ \n $ / ] ,
491+ [ '/apple-touch-icon.png' , 'image/png' , / ^ \n $ / ] ,
492+ [ '/favicon.ico' , 'image/x-icon' , / ^ \n $ / ] ,
493+ [ '/favicon-16x16.png' , 'image/png' , / ^ \n $ / ] ,
494+ [ '/favicon-32x32.png' , 'image/png' , / ^ \n $ / ] ,
495+ [ '/site.webmanifest' , 'application/manifest+json' , / ^ \n $ / ] ,
496+ ] . forEach ( ( [ path , expectedContentType , expectedContent ] ) => {
448497 it ( `${ path } file should serve expected content` , async ( ) => {
449498 // when
450499 const res = await apiFetch ( path ) ;
451500
452501 // then
453502 assert . equal ( res . status , 200 ) ;
454503 assert . match ( await res . text ( ) , expectedContent ) ;
504+ assert . equal ( res . headers . get ( 'Content-Type' ) , expectedContentType ) ;
455505 assertSecurityHeaders ( res , { csp :'central-frontend' } ) ;
456506 } ) ;
457507 } ) ;
0 commit comments