CVE-2026-4983: Verified Repro With Script Download
CVE-2026-4983: Open VSX Registry serves HTML inline enabling session/token exfiltration
CVE-2026-4983 is verified against the affected target. Vulnerability class: XSS. This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00202.
What Is CVE-2026-4983?
CVE-2026-4983 is a medium-severity stored cross-site scripting vulnerability (CWE-79) in the Open VSX Registry, where the /vscode/unpkg/ endpoint serves user-supplied HTML/SVG files from VSIX extension packages inline, enabling session/token exfiltration. Pruva reproduced it (reproduction REPRO-2026-00202).
CVE-2026-4983 Severity & CVSS Score
CVE-2026-4983 is rated medium severity, with a CVSS base score of 4.1 out of 10.
Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.
How to Reproduce CVE-2026-4983
pruva-verify REPRO-2026-00202 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00202/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-4983
- reached the target end-to-end
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
VSIX package containing an HTML file with embedded JavaScript, published via POST /api/-/publish, then accessed via GET /vscode/unpkg/{ns}/{ext}/{ver}/extension/payload.html
- POST /api/-/publish?token={token} (upload VSIX with HTML)
- GET /vscode/unpkg/{namespace}/{extension}/{version}/extension/payload.html (served with text/html, no CSP)
Alternate trigger of CVE-2026-13323: an attacker smuggles an HTML file as the extension ICON by setting "icon": "payload.html" in package.json. ExtensionProcessor.getIcon() has no icon-type validation, so the HTML is stored as the ICON FileResource and served via a different entry point (GET /api/{ns}/{ext}/{ver}/file…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-4983
The Open VSX Registry /vscode/unpkg/ endpoint serves user-supplied files extracted from published VSIX packages with insecure HTTP response headers. In versions before 1.0.2, HTML files (.html) are served with Content-Type: text/html and without a Content-Security-Policy or Content-Disposition: attachment header, causing browsers to render the HTML inline in the registry's origin context. This enables an unauthenticated attacker to upload a VSIX containing a crafted HTML payload with JavaScript that executes in the open-vsx.org origin, allowing session token exfiltration, persistent PAT generation, and unauthorized publication of malicious extension versions.
- Package/component affected:
org.eclipse.openvsx:openvsx-server— specifically the/vscode/unpkg/{namespace}/{extension}/{version}/{path}endpoint inLocalVSCodeServiceand the file-serving logic inStorageUtilService/StorageUtil - Affected versions: All versions before 1.0.2 (confirmed on v1.0.1)
- Risk level: Medium (CVSS advisory severity)
- Consequences:
- JavaScript execution in the
open-vsx.orgorigin context - Session cookie/token exfiltration from authenticated users
- Persistent Personal Access Token (PAT) generation
- Unauthorized publication of malicious extension versions
- Supply chain attack via compromised extension updates distributed to VS Code, VSCodium, Cursor, Windsurf, and compatible editors
- JavaScript execution in the
Impact Parity
- Disclosed/claimed maximum impact: Session/token exfiltration, persistent PAT generation, unauthorized publication of malicious extension versions, supply chain attack
- Reproduced impact from this run: Confirmed that the
/vscode/unpkg/endpoint serves user-controlled HTML files withContent-Type: text/htmland noContent-Security-Policyheader, which would cause a browser to render the HTML inline and execute embedded JavaScript in the registry origin. The response body contains a<script>tag that accessesdocument.cookieand would execute in the browser. - Parity:
full— the HTTP response header behavior that enables the attack is fully reproduced against the real running server. The script does not render the HTML in a browser (no headless browser), but the header evidence proves the browser would render it inline with JavaScript execution capability. - Not demonstrated: Actual browser-side JavaScript execution and token exfiltration (requires a browser environment and an authenticated victim session). The HTTP-level evidence is sufficient to prove the vulnerability.
Root Cause
The vulnerability exists in the file-serving code path of the Open VSX Registry server. When a user requests a file from a VSIX package via /vscode/unpkg/{namespace}/{extension}/{version}/{path}:
LocalVSCodeService.browse()handles the request and callsgetWebResource()to extract the requested file from the VSIX ZIP archive to a temporary path.The extracted file is served via
StorageUtilService.getFileResponse(Path path), which sets response headers usingStorageUtil.getFileType(fileName).The vulnerable code (
StorageUtil.getFileTypein v1.0.1):static MediaType getFileType(String fileName) { if (fileName.endsWith(".vsix")) return MediaType.APPLICATION_OCTET_STREAM; if (fileName.endsWith(".json")) return MediaType.APPLICATION_JSON; if (fileName.endsWith(".sigzip")) return MediaType.valueOf("application/zip"); var contentType = URLConnection.guessContentTypeFromName(fileName); if (contentType != null) return MediaType.parseMediaType(contentType); return MediaType.TEXT_PLAIN; }For
.htmlfiles,URLConnection.guessContentTypeFromName("payload.html")returnstext/html.The vulnerable response headers (
StorageUtilService.getFileResponse(Path)in v1.0.1):public ResponseEntity<StreamingResponseBody> getFileResponse(Path path) { var fileName = path.getFileName().toString(); var headers = new HttpHeaders(); headers.setContentType(StorageUtil.getFileType(fileName)); // → text/html headers.setCacheControl(StorageUtil.getCacheControl(fileName)); return ResponseEntity.ok().headers(headers).body(...); }No
Content-Security-Policyheader is set. NoContent-Disposition: attachmentheader is set for non-.vsixfiles. The browser therefore renders the HTML inline and executes any embedded JavaScript.The fix (v1.0.2) introduces
HttpHeadersUtil.createFileResponseHeaders()which:- Uses Apache Tika to detect the actual MIME type from file content
- For text-viewable types (including
text/html), forcesContent-Type: text/plain;charset=UTF-8to prevent HTML rendering - Sets
Content-Disposition: attachmentfor non-text files to force download - Always sets a strict
Content-Security-Policy: default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; sandbox - Sets
X-Content-Type-Options: nosniffandX-Frame-Options: DENY
Fix commit: The fix is included in tag v1.0.2 (commit 9491f32a6d459a4d499c5028d37c0d0386771e9f). The key changes are in:
server/src/main/java/org/eclipse/openvsx/util/HttpHeadersUtil.java(newcreateFileResponseHeadersmethod)server/src/main/java/org/eclipse/openvsx/storage/StorageUtil.java(removed vulnerablegetFileTypemethod)server/src/main/java/org/eclipse/openvsx/storage/LocalStorageService.java(uses new header util)server/src/main/java/org/eclipse/openvsx/storage/StorageUtilService.java(uses new header util)
Reproduction Steps
- Reference:
bundle/repro/reproduction_steps.sh - What the script does:
- Creates a minimal VSIX package containing
extension/payload.htmlwith a<script>tag that accessesdocument.cookie - Builds the Open VSX Registry server from source at v1.0.1 (vulnerable) and v1.0.2 (fixed) using Docker (
gradle:jdk-25-and-25for building,eclipse-temurin:25-jdkfor running) - Starts PostgreSQL 16.2 in Docker
- For each version: starts the server, inserts a user and personal access token into PostgreSQL, creates a namespace via the API, publishes the VSIX via
POST /api/-/publish?token=test_token, then requests the HTML file viaGET /vscode/unpkg/testpub/testext/1.0.0/extension/payload.html - Captures and compares the HTTP response headers between vulnerable and fixed versions
- Creates a minimal VSIX package containing
- Expected evidence:
- Vulnerable (v1.0.1):
Content-Type: text/html, noContent-Security-Policy, noContent-Disposition: attachment - Fixed (v1.0.2):
Content-Type: text/plain;charset=utf-8,Content-Security-Policy: default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; sandbox
- Vulnerable (v1.0.1):
Evidence
Log file locations
bundle/logs/vuln_v1.0.1_headers.txt— HTTP response headers from vulnerable serverbundle/logs/vuln_v1.0.1_body.html— HTML payload served by vulnerable serverbundle/logs/fixed_v1.0.2_headers.txt— HTTP response headers from fixed serverbundle/logs/fixed_v1.0.2_body.html— HTML content served by fixed server (as text/plain)bundle/logs/header_comparison.txt— Side-by-side header comparisonbundle/logs/reproduction_verdict.txt— Final verdict summarybundle/logs/server_vuln_v1.0.1.log— Vulnerable server startup logbundle/logs/server_fixed_v1.0.2.log— Fixed server startup logbundle/logs/build_v1.0.1.log/bundle/logs/build_v1.0.2.log— Build logsbundle/repro/runtime_manifest.json— Runtime evidence manifest
Key excerpts
Vulnerable (v1.0.1) response headers — serves HTML inline:
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: max-age=2592000, public
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
X-Frame-Options: DENY
Content-Type: text/html→ browser renders HTML- No
Content-Security-Policy→ JavaScript executes freely - No
Content-Disposition: attachment→ file rendered inline, not downloaded
Fixed (v1.0.2) response headers — safe serving:
HTTP/1.1 200 OK
Content-Type: text/plain;charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Cache-Control: max-age=86400, must-revalidate, public
Content-Security-Policy: default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; sandbox
X-XSS-Protection: 0
Content-Type: text/plain;charset=utf-8→ browser displays as plain textContent-Security-Policy: default-src 'none'; ...→ blocks all script execution, network requests, framing
Environment details
- Server: Open VSX Registry built from source (eclipse-openvsx/openvsx), Spring Boot with embedded Jetty
- JDK: Eclipse Temurin 25 (JDK 25)
- Database: PostgreSQL 16.2
- Build tool: Gradle 9.5.1
- Docker: Used for all components (build, database, runtime)
Recommendations / Next Steps
Upgrade to v1.0.2 or later — The fix adds comprehensive response header hardening:
- Content-type sniffing via Apache Tika (not just file extension)
- Forced
text/plainfor all text-viewable types (HTML, CSS, JS, Markdown) - Strict CSP on all served files
Content-Disposition: attachmentfor non-text filesX-Content-Type-Options: nosniffandX-Frame-Options: DENY
Audit existing extensions — Check for previously published extensions containing HTML files that may have been used for exploitation.
Consider additional mitigations:
- Add
Content-Disposition: attachmenteven for text-viewable files to force download - Implement a content allowlist for file types that can be served via
/vscode/unpkg/ - Consider serving user-uploaded content from a separate origin (e.g.,
files.open-vsx.org) to isolate from the registry origin
- Add
Testing recommendation — Add integration tests that verify response headers for various file types (
.html,.svg,.js,.css) served via the/vscode/unpkg/endpoint.
Additional Notes
- Idempotency: The reproduction script is fully self-contained and idempotent. It creates and tears down all Docker containers. Running it again produces the same results. The script cleans up all containers on exit via a trap handler.
- Negative control: The script tests both the vulnerable (v1.0.1) and fixed (v1.0.2) versions, demonstrating that the fix changes the response headers from dangerous (
text/html, no CSP) to safe (text/plain, strict CSP). - Real product proof: The reproduction uses the actual Open VSX Registry server built from source, running with a real PostgreSQL database. The extension is published through the real API (
POST /api/-/publish) and the file is served through the real/vscode/unpkg/endpoint — not a mock or harness. - Scanning disabled: The test config disables extension scanning (
ovsx.scanning.enabled: false) to allow the test VSIX to be published without being blocked by security checks. In production, scanning is enabled but does not prevent HTML files from being included in VSIX packages.
Variant Analysis & Alternative Triggers for CVE-2026-4983
A materially-distinct alternate trigger of CVE-2026-13323 was found and validated against the real Open VSX Registry server: an attacker smuggles an HTML file as the extension icon by setting "icon": "payload.html" in package.json and shipping extension/payload.html (an HTML document with <script>) inside the VSIX. ExtensionProcessor.getIcon() performs no icon-type validation, so the HTML file is extracted at publish time and stored as the ICON FileResource (named payload.html). It is then served via a different entry point and a different sink than the original repro — GET /api/{namespace}/{extension}/{version}/file/payload.html (RegistryAPI.getFile → LocalRegistryService.getFile → StorageUtilService.getFileResponse(FileResource) → LocalStorageService.getFile(FileResource)) — instead of the repro's GET /vscode/unpkg/{ns}/{ext}/{ver}/extension/payload.html (LocalVSCodeService.browse → getFileResponse(Path)).
On the vulnerable v1.0.1 server, this alternate endpoint serves the smuggled HTML with Content-Type: text/html, no Content-Security-Policy, and no Content-Disposition: attachment — i.e. the browser renders it inline in the registry origin and executes the embedded JavaScript. The icon URL is advertised in the extension metadata JSON (files.icon = .../file/payload.html), so it is exposed to victims exactly like the original CVE surface.
On the fixed v1.0.2 server, the same alternate endpoint serves the same file as Content-Type: text/plain;charset=utf-8 with the strict Content-Security-Policy: default-src 'none'; ..., X-Content-Type-Options: nosniff, and X-Frame-Options: DENY. The fix therefore covers this variant. This is an alternate trigger on the vulnerable version, NOT a bypass of the fix.
Fix Coverage / Assumptions
The v1.0.2 fix (commit 9491f32a6d459a4d499c5028d37c0d0386771e9f) centralizes all file-serving response headers into HttpHeadersUtil.createFileResponseHeaders(...) (server/src/main/java/org/eclipse/openvsx/util/HttpHeadersUtil.java) and deletes the vulnerable extension-based StorageUtil.getFileType(...).
- Invariant the fix relies on: every code path that serves a user-controlled file to an HTTP client now builds its headers via
createFileResponseHeaders, which (a) defaults toapplication/octet-stream, (b) uses Apache Tika content detection (not the extension) to classify the file, (c) forcestext/plain;charset=UTF-8for any text-viewable type (text/html,text/plain,text/markdown,text/css,text/javascript), (d) forcesContent-Disposition: attachmentfor non-text files, and (e) always sets a strictContent-Security-Policy: default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; sandboxplusnosniffandX-Frame-Options: DENY. - Code paths explicitly covered:
StorageUtilService.getFileResponse(Path)andgetFileResponse(ArrayNode);LocalStorageService.getFile(FileResource)andgetNamespaceLogo(Namespace);AwsStorageService/AzureBlobStorageService/GoogleCloudStorageService.uploadFile;UpstreamVSCodeService.streamResponseand its JSON responses. All five serving endpoints (/vscode/unpkg/,/vscode/asset/,/api/.../file/**(×2 target-platform variants),/api/{ns}/logo/{fileName}) route through these. - What the fix does NOT cover / gaps (analyzed, not exploitable for in-origin JS):
- Gap A — cloud (AWS S3) partial headers:
AwsStorageService.uploadFileonly persistsContent-Type,Content-Disposition,Cache-Control; the CSP/nosniff/X-Frame-Options are dropped on the S3 object. Not a bypass because the forcedtext/plain/octet-stream+attachment Content-Type already prevents inline HTML rendering, and the S3 origin differs fromopen-vsx.org(no session-cookie exfil). - Gap B —
application/xmlpassthrough has noContent-Disposition: served inline asapplication/xmlbut with strict CSP + nosniff; modern browsers do not execute script inapplication/xmland CSP blocks any XSLT-driven load. Not exploitable. - Gap C — publish-time extraction has no icon-type validation (this variant):
ExtensionProcessor.getIcon()is unchanged by the fix and accepts any file the manifesticonfield points at. The smuggling path therefore exists in both versions; only the serving layer was hardened. This is the basis of the variant and is mitigated at serving time on v1.0.2. - Gap D — namespace logos are pre-restricted to
image/png/image/jpegat upload (UserService.updateNamespaceDetailsLogo), so HTML/SVG logos are rejected. Not a vector (confirmed negative).
- Gap A — cloud (AWS S3) partial headers:
Variant / Alternate Trigger
Variant — HTML smuggled as the extension ICON.
- Entry point:
GET /api/{namespace}/{extension}/{version}/file/{iconName}— e.g.GET /api/vpub/vext/1.0.0/file/payload.html. Controller:RegistryAPI.getFile(server/.../RegistryAPI.java:611). - Data path (smuggling):
ExtensionProcessor.getIcon()(server/.../ExtensionProcessor.java:506) reads the file atpackage.json'siconpath with no MIME/extension check, setsiconResource.setName(iconName)("payload.html") andiconResource.setType(FileResource.ICON)(lines 524–525). The iconFileResourceis persisted and its URL advertised viagetFileUrls(..., ICON). - Serving path (v1.0.1, vulnerable sink):
LocalRegistryService.getFile(...)(LocalRegistryService.java:223) →storageUtil.getFileResponse(resource)(line 233) →LocalStorageService.getFile(FileResource)(LocalStorageService.java:89) →getFileResponseHeaders(name)(LocalStorageService.java:124) →StorageUtil.getFileType("payload.html")(StorageUtil.java:23) →URLConnection.guessContentTypeFromName→text/html, with no CSP and noContent-Disposition. - Serving path (v1.0.2, fixed): same chain, but
LocalStorageService.getFile(FileResource)now callsHttpHeadersUtil.createFileResponseHeaders(path)(LocalStorageService.java:92); Tika detects the HTML bytes astext/html→ forced totext/plain;charset=utf-8+ strict CSP. Safe. - Why it is materially distinct from the repro: different controller (
RegistryAPIvsVSCodeAPI), different handler (getFilevsbrowse), different lookup (pre-extracted DBFileResourceby name vs on-demand VSIX web-resource extraction to a tempPath), different sink overload (getFileResponse(FileResource)/LocalStorageService.getFile(FileResource)vsgetFileResponse(Path)). Same root cause (insecure headers on user content), same trust boundary (unauthenticated publisher → authenticated victim via the registry origin), same impact class.
Other candidate entry points examined and ruled out as separate vectors (they reach the same patched sinks, or are pre-restricted):
/vscode/asset/{ns}/{ext}/{ver}/{type}/{path}(LocalVSCodeService.getAsset) — samegetFileResponse(Path)sink; patched./api/{ns}/{ext}/{ver}/file/README|LICENSE|CHANGELOG— README/CHANGELOG/LICENSE are extracted only from fixed.md/.txt/extension/READMEpaths (ExtensionProcessor.java:45-46), never.html; not a vector./api/{ns}/logo/{fileName}— logo upload validated to png/jpg only; not a vector.Package/component affected:
org.eclipse.openvsx:openvsx-server—RegistryAPI.getFile/LocalRegistryService.getFile/StorageUtilService.getFileResponse(FileResource)/LocalStorageService.getFile(FileResource)(v1.0.1 insecure sink); smuggling viaExtensionProcessor.getIcon.Affected versions (as tested): alternate trigger reproduces on v1.0.1 (commit
e92a1a7a448be08570cc4c4969717ed3e2260015). Not reproducible on v1.0.2 (commit9491f32a6d459a4d499c5028d37c0d0386771e9f) — the fix covers it.Risk level / consequences (on v1.0.1): same as the parent CVE — JavaScript execution in the
open-vsx.orgorigin via a victim visiting the advertised icon URL; enables session cookie/token exfiltration, persistent PAT generation, unauthorized publication of malicious extension versions, and downstream supply-chain impact (VS Code, VSCodium, Cursor, Windsurf). Medium severity.
Impact Parity
- Disclosed/claimed maximum impact (parent CVE): session/token exfiltration, persistent PAT generation, unauthorized malicious extension publication, supply-chain attack.
- Reproduced impact from this variant run: confirmed that the alternate
/api/.../file/payload.htmlendpoint serves the attacker-controlled HTML withContent-Type: text/htmland no CSP on v1.0.1 — i.e. the exact header condition that lets a browser render the HTML inline and execute the embedded<script>(which readsdocument.cookie) in the registry origin. The smuggled icon URL is returned in the extension metadatafiles.icon, proving the surface is exposed to victims. - Parity:
full(HTTP-level header evidence is the same decisive proof used by the parent repro; the variant reaches the identical exploitable header state via a different entry point). - Not demonstrated: actual browser-side JS execution and live token exfiltration (requires a headless browser + an authenticated victim session). This is the same limitation as the parent repro and is sufficient to prove the vulnerability.
Root Cause
The same underlying bug — user-controlled files served with a Content-Type that the browser renders as active content (HTML/SVG) and without a Content-Security-Policy or Content-Disposition: attachment — is reachable from a second entry point because:
- The vulnerable v1.0.1 sink (
StorageUtil.getFileType+ manual headers inLocalStorageService.getFileResponseHeaders) was invoked from two independent call sites: the on-demand web-resource path (getFileResponse(Path), used by/vscode/unpkg/and/vscode/asset/) and the persisted-FileResourcepath (LocalStorageService.getFile(FileResource), used by/api/.../file/**). The original repro exercised only the first. - The publish pipeline (
ExtensionProcessor.getIcon) lets a publisher choose any file as the extension icon with no type check, so an HTML document can be placed into the persisted-FileResourceserving path. The fix did not changeExtensionProcessor(it is not in the v1.0.1→v1.0.2 diff), so the smuggling vector is present in both versions. - On v1.0.1, the persisted-
FileResourcesink applies the same insecuregetFileType(name)→text/htmllogic, producing the identical dangerous response as the repro.
The v1.0.2 fix removes getFileType and routes both sinks through createFileResponseHeaders, which uses Tika content detection and forces text/plain + strict CSP. Because the persisted-FileResource sink (LocalStorageService.getFile(FileResource)) was also rewired (line 92), the variant is mitigated on v1.0.2 — hence alternate trigger, not a bypass.
Fix commit: 9491f32a6d459a4d499c5028d37c0d0386771e9f (tag v1.0.2).
Reproduction Steps
- Reference:
bundle/vuln_variant/reproduction_steps.sh. - What the script does (self-contained, reuses the pre-built server jars from the repro stage at
bundle/logs/openvsx-server-v1.0.1.jarandbundle/logs/openvsx-server-v1.0.2.jar):- Builds a VSIX whose
package.jsonsets"icon": "payload.html"and that containsextension/payload.html(HTML with a<script>readingdocument.cookie). - Starts PostgreSQL 16.2 (port 5436) and, for each of v1.0.1 and v1.0.2, starts the real Open VSX server (Eclipse Temurin JDK 25), seeds a user + PAT, creates a namespace, and publishes the VSIX via
POST /api/-/publish. - Requests the variant URL
GET /api/vpub/vext/1.0.0/file/payload.htmland the control URLGET /vscode/unpkg/vpub/vext/1.0.0/extension/payload.html, capturing response headers + body for both. - Fetches
GET /api/vpub/vext/1.0.0to record the advertisedfiles.iconURL. - Computes a verdict:
INLINE_HTML=1iffContent-Type: text/htmland no CSP.
- Builds a VSIX whose
- Expected evidence:
- v1.0.1 variant:
Content-Type: text/html, no CSP, noContent-Disposition→INLINE_HTML=1(alternate trigger reproduced on vulnerable). - v1.0.2 variant:
Content-Type: text/plain;charset=utf-8, strict CSP,nosniff,X-Frame-Options: DENY→INLINE_HTML=0(fix covers it). - Exit code:
1(variant only on vulnerable; not a bypass).
- v1.0.1 variant:
Evidence
Log file locations
bundle/logs/vuln_variant/variant_repro.log— full run transcript.bundle/logs/vuln_variant/vuln_variant_headers.txt— v1.0.1 variant response headers (the key proof).bundle/logs/vuln_variant/vuln_variant_body.html— HTML body served inline by v1.0.1.bundle/logs/vuln_variant/fixed_variant_headers.txt— v1.0.2 variant response headers (proves fix).bundle/logs/vuln_variant/fixed_variant_body.html— same HTML served as text/plain by v1.0.2.bundle/logs/vuln_variant/vuln_control_headers.txt/fixed_control_headers.txt— control (/vscode/unpkg/) headers.bundle/logs/vuln_variant/vuln_publish.json/fixed_publish.json— publish responses (showfiles.icon = .../file/payload.html).bundle/logs/vuln_variant/vuln_metadata.json/fixed_metadata.json— extension metadata advertising the icon URL.bundle/logs/vuln_variant/vuln_icon_url.txt/fixed_icon_url.txt—http://localhost:8080/api/vpub/vext/1.0.0/file/payload.html.bundle/logs/vuln_variant/variant_result.json— machine-readable verdict.bundle/logs/vuln_variant/variant_verdict.txt— human-readable verdict.bundle/logs/vuln_variant/server_vuln_v1.0.1.log/server_fixed_v1.0.2.log— server startup logs.bundle/logs/vuln_variant/fixed_version.txt/latest_version.txt— exact tested commit SHAs.
Key excerpts
v1.0.1 VARIANT (/api/vpub/vext/1.0.0/file/payload.html) — inline HTML, no CSP:
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: max-age=2592000, public
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
X-Frame-Options: DENY
(no Content-Security-Policy, no Content-Disposition)
v1.0.2 VARIANT (same URL) — safe:
HTTP/1.1 200 OK
Content-Type: text/plain;charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Cache-Control: max-age=86400, must-revalidate, public
Content-Security-Policy: default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; sandbox
X-XSS-Protection: 0
Publish metadata (both versions) advertises the variant surface:
"icon":"http://localhost:8080/api/vpub/vext/1.0.0/file/payload.html"
Verdict: variant_on_vulnerable=true, variant_bypass_on_fixed=false.
Environment details
- Server: Open VSX Registry built from source (eclipse-openvsx/openvsx), Spring Boot 3.5.14, embedded Jetty, JDK 25 (Eclipse Temurin).
- Database: PostgreSQL 16.2.
- Tested commits: v1.0.1
e92a1a7a448be08570cc4c4969717ed3e2260015; v1.0.29491f32a6d459a4d499c5028d37c0d0386771e9f. - Scanning disabled in the test config (mirrors the repro config); production scanning does not reject HTML icon files (no icon-type validation exists in
ExtensionProcessor.getIcon).
Recommendations / Next Steps
- Add icon-type validation at publish in
ExtensionProcessor.getIcon()/ the publish flow: reject icons whose Tika-detected MIME type is not an image (image/png,image/jpeg,image/svg+xml). This matches the validation already present for namespace logos (UserService.updateNamespaceDetailsLogo) and would block HTML smuggling into theICONslot entirely — defense in depth on top of the serving-layer fix. - Propagate the safety-net CSP/nosniff/X-Frame-Options to cloud objects for AWS S3 (use
PutObjectRequestresponse-header overrides or a CloudFront response-headers policy), Azure, and GCS, so the CSP safety net survives the 302 redirect to the cloud origin. - Add
Content-Disposition: attachmentto the passthrough types (application/json,application/xml) increateFileResponseHeadersso the only remaining inline-serving branch is the forcedtext/plain. - Add integration tests asserting safe headers for
.html,.svg,.js,.css, a smuggled-HTML icon, and a namespace-logo SVG across/vscode/unpkg/,/vscode/asset/, and/api/.../file/**.
Additional Notes
- Idempotency: confirmed — the script was run twice; both runs completed cleanly with identical results (exit 1, identical
variant_result.json) and no leftover Docker containers (thetrap cleanup EXITremovesopenvsx-pg-variant/openvsx-runtime-variant). - Negative-control soundness: the
/vscode/unpkg/control path behaved identically to the variant on each version (text/html on v1.0.1, text/plain+CSP on v1.0.2), confirming the harness exercises the real serving code and that the fix changes behavior on both sinks. - Real product proof: uses the actual Open VSX server (built from source) with a real PostgreSQL DB; the extension is published through the real
POST /api/-/publishAPI and the file is served through the real/api/.../file/**and/vscode/unpkg/endpoints — not a mock. - Outcome classification: this is a validated alternate trigger of the same root cause on the vulnerable version. The v1.0.2 fix does cover it (the persisted-
FileResourcesink was rewired tocreateFileResponseHeaders), so it is not a bypass. No bypass of the fix was found after examining all five serving endpoints, all four storage backends, the upstream proxy, the logo upload validation, and the publish extraction paths.
CVE-2026-4983 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-4983
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-4983
FAQ: CVE-2026-4983
How does the CVE-2026-4983 stored XSS attack work?
How severe is CVE-2026-4983?
How can I reproduce CVE-2026-4983?
References for CVE-2026-4983
Authoritative sources for CVE-2026-4983 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.