diff --git a/files/nginx/odk.conf.template b/files/nginx/odk.conf.template index 1e4dab06b..896505dd6 100644 --- a/files/nginx/odk.conf.template +++ b/files/nginx/odk.conf.template @@ -165,6 +165,10 @@ server { # For that iframe to work, we'll need another path prefix (enketo-passthrough) under which we can # reach Enketo — this one will not be intercepted. location ~ ^/(?:-|enketo-passthrough)(?:/|$) { + if ($args ~* "(^|&|;)(x|%78|%58)?(f|%66|%46)(o|%6f|%4f)(r|%72|%52)(m|%6d|%4d)(=[^;&]*)?(&|;|$)" ) { + return 400; + } + rewrite ^/enketo-passthrough(/.*)?$ /-$1 break; proxy_pass http://enketo:8005; proxy_redirect off; diff --git a/test/nginx/src/mocha/nginx.spec.js b/test/nginx/src/mocha/nginx.spec.js index 1cdb9ab28..e3f6b4a1e 100644 --- a/test/nginx/src/mocha/nginx.spec.js +++ b/test/nginx/src/mocha/nginx.spec.js @@ -609,6 +609,100 @@ function standardTestSuite({ fetchHttp, fetchHttp6, apiFetch, apiFetch6, forward }); }); + describe('enketo query param filtering', () => { + [ + '/-/preview', + '/-/preview/', + '/enketo-passthrough/preview', + '/enketo-passthrough/preview/', + ].forEach(pathRoot => { + [ + 'form', + 'form=', + 'form=123', + 'form&a=1', + 'form=&a=1', + 'form=123&a=1', + 'a=1&form', + 'a=1&form=', + 'a=1&form=123', + 'a=1&form&b=2', + 'a=1&form=&b=2', + 'a=1&form=123&b=2', + 'a=1&form&form=123', + 'a=1&form=123&form=123', + + 'xform', + 'xform=', + 'xform=123', + 'xform&a=1', + 'xform=&a=1', + 'xform=123&a=1', + 'a=1&xform', + 'a=1&xform=', + 'a=1&xform=123', + 'a=1&xform&b=2', + 'a=1&xform=&b=2', + 'a=1&xform=123&b=2', + 'a=1&xform&xform=123', + 'a=1&xform=123&xform=123', + + 'form=1&xform=1&form=2&xform=2&form=3&xform=3&form=4&xform=4&form=5&xform=5&form=6&xform=6&', + + '%66orm=123', + + 'XFORM=123', + + 'form;a=1', + 'form=;a=1', + 'form=123;a=1', + 'a=1;form', + 'a=1;form=', + 'a=1;form=123', + 'a=1;form;b=2', + 'a=1;form=;b=2', + 'a=1;form=123;b=2', + 'a=1;form;form=123', + 'a=1;form=123;form=123', + ].forEach(queryString => { + const path = pathRoot + '?' + queryString; + + it(`should reject path ${path}`, async () => { + // when + const res = await apiFetch(path); + + // then + assert.equal(res.status, 400); + // and + await assertEnketoReceivedNoRequests(); + }); + }); + + [ + 'platform=mac', + 'form_id=99', + 'uniform=true', + 'a=1&deform=false&b=2', + 'information=detailed', + 'performance=good', + ].forEach(queryString => { + const path = pathRoot + '?' + queryString; + + it(`should NOT reject path ${path}`, async () => { + // when + const res = await apiFetch(path); + + // then + assert.equal(res.status, 200); + // and + await assertEnketoReceived( + { method:'GET', path:'/-/preview' + (pathRoot.endsWith('/') ? '/' : '') + '?' + queryString }, + ); + }); + }); + }); + }); + describe('blank.html', () => { [ '/blank.html',