UNPKG

tileserver-gl

Version:

Map tile server for JSON GL styles - vector and server side generated raster tiles

425 lines (376 loc) 12.8 kB
import assert from 'assert'; import { getSecureMergedParams } from '../src/serve_rendered.js'; const testStatic = function (prefix, q, format, status, scale, type, query) { if (scale) q += '@' + scale + 'x'; let path = '/styles/' + prefix + '/static/' + q + '.' + format; if (query) { path += query; } it(path + ' returns ' + status, function (done) { const test = supertest(app).get(path); if (status) test.expect(status); if (type) test.expect('Content-Type', type); test.end(done); }); }; const prefix = 'test-style'; describe('Static endpoints', function () { describe('center-based', function () { describe('valid requests', function () { describe('various formats', function () { testStatic( prefix, '0,0,0/256x256', 'png', 200, undefined, /image\/png/, ); testStatic( prefix, '0,0,0/256x256', 'jpg', 200, undefined, /image\/jpeg/, ); testStatic( prefix, '0,0,0/256x256', 'jpeg', 200, undefined, /image\/jpeg/, ); testStatic( prefix, '0,0,0/256x256', 'webp', 200, undefined, /image\/webp/, ); }); describe('different parameters', function () { testStatic(prefix, '0,0,0/300x300', 'png', 200, 2); testStatic(prefix, '0,0,0/300x300', 'png', 200, 3); testStatic(prefix, '0,0,1.5/256x256', 'png', 200); testStatic(prefix, '80,40,20/600x300', 'png', 200, 3); testStatic(prefix, '8.5,40.5,20/300x150', 'png', 200, 3); testStatic(prefix, '-8.5,-40.5,20/300x150', 'png', 200, 3); testStatic(prefix, '8,40,2@0,0/300x150', 'png', 200); testStatic(prefix, '8,40,2@180,45/300x150', 'png', 200, 2); testStatic(prefix, '8,40,2@10/300x150', 'png', 200, 3); testStatic(prefix, '8,40,2@10.3,20.4/300x300', 'png', 200); testStatic(prefix, '0,0,2@390,120/300x300', 'png', 200); }); }); describe('invalid requests return 4xx', function () { testStatic(prefix, '190,0,0/256x256', 'png', 400); testStatic(prefix, '0,86,0/256x256', 'png', 400); testStatic(prefix, '80,40,20/0x0', 'png', 400); testStatic(prefix, '0,0,0/256x256', 'gif', 400); testStatic(prefix, '0,0,0/256x256', 'png', 404, 1); testStatic(prefix, '0,0,-1/256x256', 'png', 404); testStatic(prefix, '0,0,0/256.5x256.5', 'png', 400); testStatic(prefix, '0,0,0,/256x256', 'png', 404); testStatic(prefix, '0,0,0,0,/256x256', 'png', 404); }); }); describe('area-based', function () { describe('valid requests', function () { describe('various formats', function () { testStatic( prefix, '-180,-80,180,80/10x10', 'png', 200, undefined, /image\/png/, ); testStatic( prefix, '-180,-80,180,80/10x10', 'jpg', 200, undefined, /image\/jpeg/, ); testStatic( prefix, '-180,-80,180,80/10x10', 'jpeg', 200, undefined, /image\/jpeg/, ); testStatic( prefix, '-180,-80,180,80/10x10', 'webp', 200, undefined, /image\/webp/, ); }); describe('different parameters', function () { testStatic(prefix, '-180,-90,180,90/20x20', 'png', 200, 2); testStatic(prefix, '0,0,1,1/200x200', 'png', 200, 3); testStatic(prefix, '-280,-80,0,80/280x160', 'png', 200); }); }); describe('invalid requests return 4xx', function () { testStatic(prefix, '0,87,1,88/5x2', 'png', 400); testStatic(prefix, '0,0,1,1/1x1', 'gif', 400); testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 400); }); }); describe('autofit path', function () { describe('valid requests', function () { testStatic( prefix, 'auto/256x256', 'png', 200, undefined, /image\/png/, '?path=10,10|20,20', ); describe('different parameters', function () { testStatic( prefix, 'auto/20x20', 'png', 200, 2, /image\/png/, '?path=10,10|20,20', ); testStatic( prefix, 'auto/200x200', 'png', 200, 3, /image\/png/, '?path=-10,-10|-20,-20', ); }); describe('encoded path', function () { testStatic( prefix, 'auto/20x20', 'png', 200, 2, /image\/png/, '?path=' + encodeURIComponent('enc:{{biGwvyGoUi@s_A|{@'), ); }); }); describe('invalid requests return 4xx', function () { testStatic(prefix, 'auto/256x256', 'png', 400); testStatic( prefix, 'auto/256x256', 'png', 400, undefined, undefined, '?path=invalid', ); testStatic( prefix, 'auto/2560x2560', 'png', 400, undefined, undefined, '?path=10,10|20,20', ); }); }); const staticAutoPath = '/styles/' + prefix + '/static/auto/256x256.png'; describe('POST static (path in body, issue #408)', function () { describe('valid requests', function () { it('POST with path in JSON body returns 200 and image/png', function (done) { supertest(app) .post(staticAutoPath) .set('Content-Type', 'application/json') .send({ path: '10,10|20,20' }) .expect(200) .expect('Content-Type', /image\/png/) .end(done); }); it('POST with long path in body succeeds (avoids URL length limit)', function (done) { const manyCoords = Array.from( { length: 200 }, (_, i) => `${10 + i * 0.1},${20 + i * 0.1}`, ).join('|'); supertest(app) .post(staticAutoPath) .set('Content-Type', 'application/json') .send({ path: manyCoords }) .expect(200) .expect('Content-Type', /image\/png/) .end(done); }); it('POST with scale and path in body returns 200', function (done) { supertest(app) .post('/styles/' + prefix + '/static/auto/20x20@2x.png') .set('Content-Type', 'application/json') .send({ path: '10,10|20,20' }) .expect(200) .expect('Content-Type', /image\/png/) .end(done); }); }); describe('invalid requests return 4xx', function () { it('POST with path array in body returns 200', function (done) { supertest(app) .post(staticAutoPath) .set('Content-Type', 'application/json') .send({ path: ['10,10|20,20', '-5,-5|5,5'] }) .expect(200) .expect('Content-Type', /image\/png/) .end(done); }); it('POST auto without path in body returns 400', function (done) { supertest(app) .post(staticAutoPath) .set('Content-Type', 'application/json') .send({}) .expect(400, done); }); it('POST with invalid JSON body returns 400 or 415', function (done) { supertest(app) .post(staticAutoPath) .set('Content-Type', 'application/json') .send('not json') .expect((res) => { expect([400, 415]).to.include(res.status); }) .end(done); }); }); it('POST to tile URL returns 405 Method Not Allowed', function (done) { supertest(app) .post('/styles/' + prefix + '/0/0/0.png') .set('Content-Type', 'application/json') .send({}) .expect(405, done); }); it('POST to tile URL with invalid JSON returns 405', function (done) { supertest(app) .post('/styles/' + prefix + '/0/0/0.png') .set('Content-Type', 'application/json') // Intentionally send invalid JSON string .send('not json') .expect(405, done); }); it('POST to tile URL with oversized JSON body returns 405', function (done) { // Construct a JSON body that exceeds the configured 5 MB JSON parser limit const largeString = 'x'.repeat(6 * 1024 * 1024); // ~6 MB supertest(app) .post(staticAutoPath) .set('Content-Type', 'application/json') .send({ data: largeString }) .expect(413, done); }); }); describe('getSecureMergedParams Logic & Security', function () { it('should maintain multiple values in query (Express array style)', () => { const query = { path: ['10,10|20,20', '10,20|20,10'], marker: ['10,15', '20,15'], latlng: '', }; const result = getSecureMergedParams(query, {}); assert.deepStrictEqual(result.path, ['10,10|20,20', '10,20|20,10']); assert.deepStrictEqual(result.marker, ['10,15', '20,15']); assert.strictEqual(result.latlng, ''); }); it('should maintain multiple values in body (Express array style)', () => { const body = { path: ['10,20|20,10', '10,10|20,20'], marker: ['20,15', '10,15'], latlng: '', }; const result = getSecureMergedParams({}, body); assert.deepStrictEqual(result.path, ['10,20|20,10', '10,10|20,20']); assert.deepStrictEqual(result.marker, ['20,15', '10,15']); assert.strictEqual(result.latlng, ''); }); it('should merge a single value and a single value into an array', () => { const query = { path: '10,10|20,20' }; const body = { path: '10,20|20,10' }; const result = getSecureMergedParams(query, body); assert.deepStrictEqual(result.path, ['10,10|20,20', '10,20|20,10']); }); it('should merge a single value and an array value', () => { const query = { path: '10,10|20,20' }; const body = { path: ['10,20|20,10', '10,10|15,20|20,10'] }; const result = getSecureMergedParams(query, body); assert.deepStrictEqual(result.path, [ '10,10|20,20', '10,20|20,10', '10,10|15,20|20,10', ]); }); it('should merge an array value and an array value', () => { const query = { path: ['10,10|20,20', '10,20|20,10'] }; const body = { path: ['10,10|15,20|20,10', '5,5|10,10'] }; const result = getSecureMergedParams(query, body); assert.deepStrictEqual(result.path, [ '10,10|20,20', '10,20|20,10', '10,10|15,20|20,10', '5,5|10,10', ]); }); it('should preserve empty strings (e.g. ?latlng&latlng)', () => { const query = { latlng: '' }; const body = { latlng: '' }; const result = getSecureMergedParams(query, body); assert.deepStrictEqual(result.latlng, ['', '']); }); it('should throw 400 error on nested objects (Deep Object vulnerability)', () => { const body = { path: { lat: 10, lon: 10 } }; assert.throws( () => getSecureMergedParams({}, body), /nested objects are not allowed/, ); }); it('should ignore unallowed keys like "__proto__"', () => { const body = JSON.parse('{"__proto__": true, "path": "10,10|20,20"}'); const result = getSecureMergedParams({}, body); assert.strictEqual(result.__proto__, undefined); // It is stripped assert.strictEqual(result.path, '10,10|20,20'); assert.strictEqual(Object.getPrototypeOf(result), null); }); it('POST with Prototype Pollution in JSON string returns 400', function (done) { const maliciousPayload = '{"__proto__": {"admin": true}, }'; supertest(app) .post(staticAutoPath) .set('Content-Type', 'application/json') .send(maliciousPayload) .expect(400) .expect((res) => { // secure-json-parse error message usually contains these terms assert.ok( res.text.includes('Invalid JSON') || res.text.includes('forbidden'), ); }) .end(done); }); it('should throw 400 error if the input itself is an array', () => { const body = ['not', 'an', 'object']; assert.throws(() => getSecureMergedParams({}, body), /Invalid data/); }); it('should ensure the result has no prototype', () => { const result = getSecureMergedParams({ a: 1 }, { b: 2 }); assert.strictEqual(Object.getPrototypeOf(result), null); assert.strictEqual(result.toString, undefined); }); }); });