Files
OpenFrontIO/package.json
T
Mehmet KOZAN b8137927a6 Security: Fix Critical XSS in NewsModal (CVE GHSA-rpr9-rxv7-x643) (#3932)
Subject: Security Vulnerability Report: Critical XSS in OpenFront.io via
sanitize-html (CVE GHSA-rpr9-rxv7-x643)

Hello OpenFront Development Team,

While reviewing the OpenFront.io project, I discovered a critical
Cross-Site Scripting (XSS) vulnerability on the client side. I am
responsibly disclosing this issue to you along with technical details
and a remediation plan so it can be addressed.

Vulnerability Summary
- Vulnerability Type: Cross-Site Scripting (XSS) / Mutation XSS
- Affected Components: src/client/NewsModal.ts,
src/client/components/NewsBox.ts
- Affected Dependency: sanitize-html v2.17.0 (imported via lit-markdown)
- CVE Reference: GHSA-rpr9-rxv7-x643 (CVSS Score: 9.3)

Technical Details
The "News" (Changelog) modal in the game uses the lit-markdown package
to parse markdown content. This package depends on sanitize-html
v2.17.0.

This specific version of sanitize-html has a known parsing flaw when
handling the `<xmp>` tag. When malicious HTML is wrapped inside an
`<xmp>` tag, the sanitization filter misinterprets it and fails to
properly strip the inner HTML. As a result, when the sanitized content
is injected into the DOM, the browser executes the inner HTML.

Proof of Concept (PoC)
If the changelog.md file (or the network response) is manipulated to
include the following payload, the malicious code bypasses sanitization
and executes in the context of the application:

`<xmp><img src=x onerror="alert('System compromised')"></xmp>`

In local testing, injecting this payload directly into the markdown
property of the news-modal component resulted in the `<img>` tag
bypassing the filter and rendering successfully in the DOM.

Impact
This vulnerability introduces a high-risk Stored XSS vector. If an
attacker compromises the server or the CDN hosting the changelog.md
file, or performs a Man-in-the-Middle (MitM) attack:
- Arbitrary JavaScript can be executed in the browsers of all players
who open the News modal.
- Session tokens and authentication data can be stolen.
- Attackers can perform unauthorized actions on behalf of the players
(e.g., disbanding clans or altering settings).

Remediation
The fix is straightforward and requires updating the sanitize-html
library to version 2.17.4 or higher.

You can enforce this update by adding an overrides block to your
package.json:

"overrides": { 
  "sanitize-html": ">=2.17.4" 
}

After updating the package.json, running npm install will apply the
patch.

I am disclosing this vulnerability responsibly and will keep the details
private until a patch has been released. Please let me know if you need
any further information or assistance with the fix.

Best regards,

Mehmet Kozan
Security Researcher
Email: twanske1@gmail.com

---

## Description:

This PR addresses the critical XSS vulnerability detailed above. By
enforcing `sanitize-html` to be version `>=2.17.4` via the `overrides`
block in `package.json`, the `<xmp>` tag parsing flaw is patched. No UI
changes or new text strings were added.

## Please complete the following:

- [ ] I have added screenshots for all UI updates *(N/A - Security patch
in package.json)*
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file *(N/A)*
- [ ] I have added relevant tests to the test directory *(N/A)*
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

hz.mehmetsultan
2026-05-15 16:37:02 -07:00

123 lines
4.2 KiB
JSON

{
"name": "openfront-client",
"scripts": {
"build-dev": "concurrently \"tsc --noEmit\" \"vite build --mode development\"",
"build-prod": "concurrently --kill-others-on-fail \"tsc --noEmit\" \"vite build\"",
"start:client": "vite",
"start:server": "tsx src/server/Server.ts",
"start:server-dev": "cross-env GAME_ENV=dev NUM_WORKERS=2 TURNSTILE_SITE_KEY=1x00000000000000000000AA API_KEY=WARNING_DEV_API_KEY_DO_NOT_USE_IN_PRODUCTION DOMAIN=localhost GIT_COMMIT=DEV tsx src/server/Server.ts",
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
"dev:staging": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
"dev:prod": "cross-env GAME_ENV=dev API_DOMAIN=api.openfront.io concurrently \"npm run start:client\" \"npm run start:server-dev\"",
"docs:map-generator": "cd map-generator && go doc -cmd -u -all",
"tunnel": "npm run build-prod && npm run start:server",
"test": "vitest run && vitest run tests/server",
"perf": "npx tsx tests/perf/run-all.ts",
"test:coverage": "vitest run --coverage",
"format": "prettier --ignore-unknown --write .",
"format:map-generator": "cd map-generator && go fmt .",
"lint": "eslint",
"lint:fix": "eslint --fix",
"prepare": "husky",
"gen-maps": "cd map-generator && go run . && npm run format",
"inst": "npm ci --ignore-scripts"
},
"lint-staged": {
"**/*": [
"eslint --fix",
"prettier --ignore-unknown --write"
]
},
"devDependencies": {
"@datastructures-js/priority-queue": "^6.3.5",
"@eslint/compat": "^2.0.5",
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.2.4",
"@types/benchmark": "^2.1.5",
"@types/d3": "^7.4.3",
"@types/ejs": "^3.1.5",
"@types/express": "^5.0.6",
"@types/hammerjs": "^2.0.46",
"@types/howler": "^2.2.12",
"@types/js-yaml": "^4.0.9",
"@types/msgpack5": "^3.4.6",
"@types/node": "^24.12.0",
"@types/pg": "^8.20.0",
"@types/seedrandom": "^3.0.8",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^4.1.5",
"@vitest/ui": "^4.1.5",
"autoprefixer": "^10.5.0",
"benchmark": "^2.1.4",
"canvas": "^3.2.3",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"d3": "^7.9.0",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-formatter-gha": "^2.0.1",
"glob": "^13.0.6",
"globals": "^17.6.0",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^16.4.0",
"lit": "^3.3.2",
"lit-markdown": "^1.3.2",
"mrmime": "^2.0.1",
"pixi-filters": "^6.1.5",
"pixi.js": "^8.18.1",
"prettier": "^3.8.3",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-sh": "^0.18.1",
"tailwindcss": "^4.2.4",
"tsconfig-paths": "^4.2.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.1",
"vite": "^8.0.10",
"vite-plugin-html": "^3.2.2",
"vitest": "^4.1.5",
"vitest-canvas-mock": "^1.1.4"
},
"dependencies": {
"@lit-labs/virtualizer": "^2.1.1",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-logs": "^0.216.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.216.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.216.0",
"@opentelemetry/resources": "^2.7.1",
"@opentelemetry/sdk-logs": "^0.216.0",
"@opentelemetry/sdk-metrics": "^2.7.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@opentelemetry/winston-transport": "^0.26.0",
"@types/compression": "^1.8.1",
"colord": "^2.9.3",
"colorjs.io": "^0.6.1",
"compression": "^1.8.1",
"dompurify": "^3.4.2",
"dotenv": "^17.4.2",
"ejs": "^5.0.2",
"express": "^5.2.1",
"express-rate-limit": "^8.5.1",
"fastpriorityqueue": "^0.8.0",
"howler": "^2.2.4",
"intl-messageformat": "^11.2.3",
"ip-anonymize": "^0.1.0",
"jose": "^6.2.3",
"js-yaml": "^4.1.1",
"limiter": "^3.0.0",
"nanoid": "^5.1.11",
"node-html-parser": "^7.1.0",
"obscenity": "^0.4.6",
"seedrandom": "^3.0.5",
"ts-node": "^10.9.2",
"tsx": "^4.21.0",
"winston": "^3.19.0",
"ws": "^8.20.0",
"zod": "^4.4.2"
},
"overrides": {
"sanitize-html": ">=2.17.4"
},
"type": "module"
}