Build/runtime hardening and dependency security updates (#286)

* Simplify RSS freshness update to static import

* Refine vendor chunking for map stack in Vite build

* Patch transitive XML parser vulnerability via npm override

* Shim Node child_process for browser bundle warnings

* Filter known onnxruntime eval warning in Vite build

* test: add loaders XML/WMS parser regression coverage

* chore: align fast-xml-parser override with merged dependency set

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
This commit is contained in:
toasterbook88
2026-02-24 03:21:03 -05:00
committed by GitHub
parent da680d7397
commit b667b189ff
7 changed files with 192 additions and 40 deletions

View File

@@ -0,0 +1,123 @@
import { strict as assert } from 'node:assert';
import test from 'node:test';
import { XMLLoader } from '@loaders.gl/xml';
import { WMSCapabilitiesLoader, WMSErrorLoader, _WMSFeatureInfoLoader } from '@loaders.gl/wms';
const WMS_CAPABILITIES_XML = `<?xml version="1.0" encoding="UTF-8"?>
<WMS_Capabilities version="1.3.0">
<Service>
<Name>WMS</Name>
<Title>Test Service</Title>
<KeywordList>
<Keyword>alerts</Keyword>
<Keyword>world</Keyword>
</KeywordList>
</Service>
<Capability>
<Request>
<GetMap>
<Format>image/png</Format>
<Format>image/jpeg</Format>
</GetMap>
</Request>
<Exception>
<Format>application/vnd.ogc.se_xml</Format>
</Exception>
<Layer>
<Title>Root Layer</Title>
<CRS>EPSG:4326</CRS>
<EX_GeographicBoundingBox>
<westBoundLongitude>-180</westBoundLongitude>
<eastBoundLongitude>180</eastBoundLongitude>
<southBoundLatitude>-90</southBoundLatitude>
<northBoundLatitude>90</northBoundLatitude>
</EX_GeographicBoundingBox>
<Layer queryable="1">
<Name>alerts</Name>
<Title>Alerts</Title>
<BoundingBox CRS="EPSG:4326" minx="-10" miny="-20" maxx="30" maxy="40" />
<Dimension name="time" units="ISO8601" default="2024-01-01" nearestValue="1">
2024-01-01/2024-12-31/P1D
</Dimension>
</Layer>
</Layer>
</Capability>
</WMS_Capabilities>`;
test('XMLLoader keeps namespace stripping + array paths stable', () => {
const xml = '<root><ns:Child attr="x">ok</ns:Child><ns:Child attr="y">yo</ns:Child></root>';
const parsed = XMLLoader.parseTextSync(xml, {
xml: {
removeNSPrefix: true,
arrayPaths: ['root.Child'],
},
});
assert.deepEqual(parsed, {
root: {
Child: [
{ value: 'ok', attr: 'x' },
{ value: 'yo', attr: 'y' },
],
},
});
});
test('WMSCapabilitiesLoader parses core typed fields from XML capabilities', () => {
const parsed = WMSCapabilitiesLoader.parseTextSync(WMS_CAPABILITIES_XML);
assert.equal(parsed.version, '1.3.0');
assert.equal(parsed.name, 'WMS');
assert.deepEqual(parsed.requests.GetMap.mimeTypes, ['image/png', 'image/jpeg']);
assert.equal(parsed.layers.length, 1);
const rootLayer = parsed.layers[0];
assert.deepEqual(rootLayer.geographicBoundingBox, [[-180, -90], [180, 90]]);
const alertsLayer = rootLayer.layers[0];
assert.equal(alertsLayer.name, 'alerts');
assert.equal(alertsLayer.queryable, true);
assert.deepEqual(alertsLayer.boundingBoxes[0], {
crs: 'EPSG:4326',
boundingBox: [[-10, -20], [30, 40]],
});
assert.deepEqual(alertsLayer.dimensions[0], {
name: 'time',
units: 'ISO8601',
extent: '2024-01-01/2024-12-31/P1D',
defaultValue: '2024-01-01',
nearestValue: true,
});
});
test('WMSErrorLoader extracts namespaced error text and honors throw options', () => {
const namespacedErrorXml =
'<?xml version="1.0"?><ogc:ServiceExceptionReport><ogc:ServiceException code="LayerNotDefined">Bad layer</ogc:ServiceException></ogc:ServiceExceptionReport>';
const defaultMessage = WMSErrorLoader.parseTextSync(namespacedErrorXml);
assert.equal(defaultMessage, 'WMS Service error: Bad layer');
const minimalMessage = WMSErrorLoader.parseTextSync(namespacedErrorXml, {
wms: { minimalErrors: true },
});
assert.equal(minimalMessage, 'Bad layer');
assert.throws(
() => WMSErrorLoader.parseTextSync(namespacedErrorXml, { wms: { throwOnError: true } }),
/WMS Service error: Bad layer/
);
});
test('WMS feature info parsing remains stable for single and repeated FIELDS nodes', () => {
const singleFieldsXml = '<?xml version="1.0"?><FeatureInfoResponse><FIELDS id="1" label="one"/></FeatureInfoResponse>';
const manyFieldsXml = '<?xml version="1.0"?><FeatureInfoResponse><FIELDS id="1"/><FIELDS id="2"/></FeatureInfoResponse>';
const single = _WMSFeatureInfoLoader.parseTextSync(singleFieldsXml);
const many = _WMSFeatureInfoLoader.parseTextSync(manyFieldsXml);
assert.equal(single.features.length, 1);
assert.deepEqual(single.features[0]?.attributes, { id: '1', label: 'one' });
assert.equal(many.features.length, 2);
assert.equal(many.features[0]?.attributes?.id, '1');
assert.equal(many.features[1]?.attributes?.id, '2');
});

34
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "world-monitor",
"version": "2.5.3",
"version": "2.5.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "world-monitor",
"version": "2.5.3",
"version": "2.5.6",
"license": "AGPL-3.0-only",
"dependencies": {
"@deck.gl/aggregation-layers": "^9.2.6",
@@ -3008,36 +3008,6 @@
"@loaders.gl/core": "^4.3.0"
}
},
"node_modules/@loaders.gl/xml/node_modules/fast-xml-parser": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^1.1.1"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/@loaders.gl/xml/node_modules/strnum": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/@loaders.gl/zip": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@loaders.gl/zip/-/zip-4.3.4.tgz",

View File

@@ -26,7 +26,7 @@
"test:e2e:runtime": "VITE_VARIANT=full playwright test e2e/runtime-fetch.spec.ts",
"test:e2e": "npm run test:e2e:runtime && npm run test:e2e:full && npm run test:e2e:tech && npm run test:e2e:finance",
"test:data": "node --test tests/*.test.mjs",
"test:sidecar": "node --test src-tauri/sidecar/local-api-server.test.mjs api/_cors.test.mjs api/youtube/embed.test.mjs api/cyber-threats.test.mjs api/usni-fleet.test.mjs scripts/ais-relay-rss.test.cjs",
"test:sidecar": "node --test src-tauri/sidecar/local-api-server.test.mjs api/_cors.test.mjs api/youtube/embed.test.mjs api/cyber-threats.test.mjs api/usni-fleet.test.mjs scripts/ais-relay-rss.test.cjs api/loaders-xml-wms-regression.test.mjs",
"test:e2e:visual:full": "VITE_VARIANT=full playwright test -g \"matches golden screenshots per layer and zoom\"",
"test:e2e:visual:tech": "VITE_VARIANT=tech playwright test -g \"matches golden screenshots per layer and zoom\"",
"test:e2e:visual": "npm run test:e2e:visual:full && npm run test:e2e:visual:tech",
@@ -82,5 +82,8 @@
"posthog-js": "^1.352.0",
"topojson-client": "^3.1.0",
"youtubei.js": "^16.0.1"
},
"overrides": {
"fast-xml-parser": "^5.3.7"
}
}

View File

@@ -4,6 +4,7 @@ import { chunkArray, fetchWithProxy } from '@/utils';
import { classifyByKeyword, classifyWithAI } from './threat-classifier';
import { inferGeoHubsFromTitle } from './geo-hub-index';
import { getPersistentCache, setPersistentCache } from './persistent-cache';
import { dataFreshness } from './data-freshness';
import { ingestHeadlines } from './trending-keywords';
import { getCurrentLanguage } from './i18n';
@@ -316,9 +317,7 @@ export async function fetchCategoryFeeds(
}
if (totalItems > 0) {
import('./data-freshness').then(({ dataFreshness }) => {
dataFreshness.recordUpdate('rss', totalItems);
});
dataFreshness.recordUpdate('rss', totalItems);
}
return ensureSortedDescending();

View File

@@ -0,0 +1,14 @@
/**
* Browser shim for @loaders.gl/worker-utils ChildProcessProxy.
* loaders.gl exposes this Node-only utility from its root index, which can
* trigger bundler warnings in browser builds even when not used at runtime.
*/
export default class ChildProcessProxy {
async start(): Promise<{}> {
throw new Error('ChildProcessProxy is not available in browser environments.');
}
async stop(): Promise<void> {}
async exit(_statusCode: number = 0): Promise<void> {}
}

View File

@@ -0,0 +1,9 @@
/**
* Browser shim for Node's `child_process` module.
* Some transitive dependencies reference it even in browser bundles.
*/
export function spawn(): never {
throw new Error('child_process.spawn is not available in browser environments.');
}
export default { spawn };

View File

@@ -625,10 +625,32 @@ export default defineConfig({
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
child_process: resolve(__dirname, 'src/shims/child-process.ts'),
'node:child_process': resolve(__dirname, 'src/shims/child-process.ts'),
'@loaders.gl/worker-utils/dist/lib/process-utils/child-process-proxy.js': resolve(
__dirname,
'src/shims/child-process-proxy.ts'
),
},
},
build: {
// Geospatial bundles (maplibre/deck) are expected to be large even when split.
// Raise warning threshold to reduce noisy false alarms in CI.
chunkSizeWarningLimit: 1200,
rollupOptions: {
onwarn(warning, warn) {
// onnxruntime-web ships a minified browser bundle that intentionally uses eval.
// Keep build logs focused by filtering this known third-party warning only.
if (
warning.code === 'EVAL'
&& typeof warning.id === 'string'
&& warning.id.includes('/onnxruntime-web/dist/ort-web.min.js')
) {
return;
}
warn(warning);
},
input: {
main: resolve(__dirname, 'index.html'),
settings: resolve(__dirname, 'settings.html'),
@@ -637,11 +659,23 @@ export default defineConfig({
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('/@xenova/transformers/') || id.includes('/onnxruntime-web/')) {
return 'ml';
if (id.includes('/@xenova/transformers/')) {
return 'transformers';
}
if (id.includes('/@deck.gl/') || id.includes('/maplibre-gl/') || id.includes('/h3-js/')) {
return 'map';
if (id.includes('/onnxruntime-web/')) {
return 'onnxruntime';
}
if (id.includes('/maplibre-gl/')) {
return 'maplibre';
}
if (
id.includes('/@deck.gl/')
|| id.includes('/@luma.gl/')
|| id.includes('/@loaders.gl/')
|| id.includes('/@math.gl/')
|| id.includes('/h3-js/')
) {
return 'deck-stack';
}
if (id.includes('/d3/')) {
return 'd3';