Skip to content

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.

REPRO-2026-00202 XSS Variant found Jul 2, 2026 CVE entry ↗ .txt
Severity
MEDIUM
CVSS
4.1
Confidence
HIGH
Reproduced in
36m 21s
Tool calls
250
Spend
$4.48
01 · Overview

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).

02 · Severity & CVSS

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 threat level
4.1 / 10 CVSS base
Weakness CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.

How to Reproduce CVE-2026-4983

$ pruva-verify REPRO-2026-00202
or 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
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-4983

Information disclosure — reproduced
  • reached the target end-to-end
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
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

Attack chain
  1. POST /api/-/publish?token={token} (upload VSIX with HTML)
  2. GET /vscode/unpkg/{namespace}/{extension}/{version}/extension/payload.html (served with text/html, no CSP)
Variants tested

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 603 events · 250 tool calls · 36 min
36 minDuration
250Tool calls
152Reasoning steps
603Events
Agent activity over 36 min
Support
50
Hypothesis
2
Repro
386
Judge
39
Variant
122
0:0036:21

Root Cause and Exploit Chain for CVE-2026-4983

Versions: All versions before 1.0.2 (confirmed on v1.0.1)

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 in LocalVSCodeService and the file-serving logic in StorageUtilService / 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.org origin 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

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 with Content-Type: text/html and no Content-Security-Policy header, 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 accesses document.cookie and 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}:

  1. LocalVSCodeService.browse() handles the request and calls getWebResource() to extract the requested file from the VSIX ZIP archive to a temporary path.

  2. The extracted file is served via StorageUtilService.getFileResponse(Path path), which sets response headers using StorageUtil.getFileType(fileName).

  3. The vulnerable code (StorageUtil.getFileType in 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 .html files, URLConnection.guessContentTypeFromName("payload.html") returns text/html.

  4. 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-Policy header is set. No Content-Disposition: attachment header is set for non-.vsix files. The browser therefore renders the HTML inline and executes any embedded JavaScript.

  5. 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), forces Content-Type: text/plain;charset=UTF-8 to prevent HTML rendering
    • Sets Content-Disposition: attachment for 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: nosniff and X-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 (new createFileResponseHeaders method)
  • server/src/main/java/org/eclipse/openvsx/storage/StorageUtil.java (removed vulnerable getFileType method)
  • 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

  1. Reference: bundle/repro/reproduction_steps.sh
  2. What the script does:
    • Creates a minimal VSIX package containing extension/payload.html with a <script> tag that accesses document.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-25 for building, eclipse-temurin:25-jdk for 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 via GET /vscode/unpkg/testpub/testext/1.0.0/extension/payload.html
    • Captures and compares the HTTP response headers between vulnerable and fixed versions
  3. Expected evidence:
    • Vulnerable (v1.0.1): Content-Type: text/html, no Content-Security-Policy, no Content-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

Evidence

Log file locations
  • bundle/logs/vuln_v1.0.1_headers.txt — HTTP response headers from vulnerable server
  • bundle/logs/vuln_v1.0.1_body.html — HTML payload served by vulnerable server
  • bundle/logs/fixed_v1.0.2_headers.txt — HTTP response headers from fixed server
  • bundle/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 comparison
  • bundle/logs/reproduction_verdict.txt — Final verdict summary
  • bundle/logs/server_vuln_v1.0.1.log — Vulnerable server startup log
  • bundle/logs/server_fixed_v1.0.2.log — Fixed server startup log
  • bundle/logs/build_v1.0.1.log / bundle/logs/build_v1.0.2.log — Build logs
  • bundle/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 text
  • Content-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

  1. 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/plain for all text-viewable types (HTML, CSS, JS, Markdown)
    • Strict CSP on all served files
    • Content-Disposition: attachment for non-text files
    • X-Content-Type-Options: nosniff and X-Frame-Options: DENY
  2. Audit existing extensions — Check for previously published extensions containing HTML files that may have been used for exploitation.

  3. Consider additional mitigations:

    • Add Content-Disposition: attachment even 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
  4. 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

Versions: versions (as tested): alternate trigger reproduces on v1.0.1 (commit e92a1a7a448be08570cc4c4969717ed3e2260015). Not reproducible on v1.0.2 (commit 9491f32a6d459a4d499c5028d37c0d0386771e9f) — the fix covers it.

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.getFileLocalRegistryService.getFileStorageUtilService.getFileResponse(FileResource)LocalStorageService.getFile(FileResource)) — instead of the repro's GET /vscode/unpkg/{ns}/{ext}/{ver}/extension/payload.html (LocalVSCodeService.browsegetFileResponse(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 to application/octet-stream, (b) uses Apache Tika content detection (not the extension) to classify the file, (c) forces text/plain;charset=UTF-8 for any text-viewable type (text/html, text/plain, text/markdown, text/css, text/javascript), (d) forces Content-Disposition: attachment for non-text files, and (e) always sets a strict Content-Security-Policy: default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; sandbox plus nosniff and X-Frame-Options: DENY.
  • Code paths explicitly covered: StorageUtilService.getFileResponse(Path) and getFileResponse(ArrayNode); LocalStorageService.getFile(FileResource) and getNamespaceLogo(Namespace); AwsStorageService/AzureBlobStorageService/GoogleCloudStorageService.uploadFile; UpstreamVSCodeService.streamResponse and 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.uploadFile only persists Content-Type, Content-Disposition, Cache-Control; the CSP/nosniff/X-Frame-Options are dropped on the S3 object. Not a bypass because the forced text/plain/octet-stream+attachment Content-Type already prevents inline HTML rendering, and the S3 origin differs from open-vsx.org (no session-cookie exfil).
    • Gap B — application/xml passthrough has no Content-Disposition: served inline as application/xml but with strict CSP + nosniff; modern browsers do not execute script in application/xml and 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 manifest icon field 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/jpeg at upload (UserService.updateNamespaceDetailsLogo), so HTML/SVG logos are rejected. Not a vector (confirmed negative).

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 at package.json's icon path with no MIME/extension check, sets iconResource.setName(iconName) ("payload.html") and iconResource.setType(FileResource.ICON) (lines 524–525). The icon FileResource is persisted and its URL advertised via getFileUrls(..., 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.guessContentTypeFromNametext/html, with no CSP and no Content-Disposition.
  • Serving path (v1.0.2, fixed): same chain, but LocalStorageService.getFile(FileResource) now calls HttpHeadersUtil.createFileResponseHeaders(path) (LocalStorageService.java:92); Tika detects the HTML bytes as text/html → forced to text/plain;charset=utf-8 + strict CSP. Safe.
  • Why it is materially distinct from the repro: different controller (RegistryAPI vs VSCodeAPI), different handler (getFile vs browse), different lookup (pre-extracted DB FileResource by name vs on-demand VSIX web-resource extraction to a temp Path), different sink overload (getFileResponse(FileResource)/LocalStorageService.getFile(FileResource) vs getFileResponse(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) — same getFileResponse(Path) sink; patched.

  • /api/{ns}/{ext}/{ver}/file/README|LICENSE|CHANGELOG — README/CHANGELOG/LICENSE are extracted only from fixed .md/.txt/extension/README paths (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-serverRegistryAPI.getFile / LocalRegistryService.getFile / StorageUtilService.getFileResponse(FileResource) / LocalStorageService.getFile(FileResource) (v1.0.1 insecure sink); smuggling via ExtensionProcessor.getIcon.

  • Affected versions (as tested): alternate trigger reproduces on v1.0.1 (commit e92a1a7a448be08570cc4c4969717ed3e2260015). Not reproducible on v1.0.2 (commit 9491f32a6d459a4d499c5028d37c0d0386771e9f) — the fix covers it.

  • Risk level / consequences (on v1.0.1): same as the parent CVE — JavaScript execution in the open-vsx.org origin 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.html endpoint serves the attacker-controlled HTML with Content-Type: text/html and 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 reads document.cookie) in the registry origin. The smuggled icon URL is returned in the extension metadata files.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:

  1. The vulnerable v1.0.1 sink (StorageUtil.getFileType + manual headers in LocalStorageService.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-FileResource path (LocalStorageService.getFile(FileResource), used by /api/.../file/**). The original repro exercised only the first.
  2. 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-FileResource serving path. The fix did not change ExtensionProcessor (it is not in the v1.0.1→v1.0.2 diff), so the smuggling vector is present in both versions.
  3. On v1.0.1, the persisted-FileResource sink applies the same insecure getFileType(name)text/html logic, 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

  1. Reference: bundle/vuln_variant/reproduction_steps.sh.
  2. What the script does (self-contained, reuses the pre-built server jars from the repro stage at bundle/logs/openvsx-server-v1.0.1.jar and bundle/logs/openvsx-server-v1.0.2.jar):
    • Builds a VSIX whose package.json sets "icon": "payload.html" and that contains extension/payload.html (HTML with a <script> reading document.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.html and the control URL GET /vscode/unpkg/vpub/vext/1.0.0/extension/payload.html, capturing response headers + body for both.
    • Fetches GET /api/vpub/vext/1.0.0 to record the advertised files.icon URL.
    • Computes a verdict: INLINE_HTML=1 iff Content-Type: text/html and no CSP.
  3. Expected evidence:
    • v1.0.1 variant: Content-Type: text/html, no CSP, no Content-DispositionINLINE_HTML=1 (alternate trigger reproduced on vulnerable).
    • v1.0.2 variant: Content-Type: text/plain;charset=utf-8, strict CSP, nosniff, X-Frame-Options: DENYINLINE_HTML=0 (fix covers it).
    • Exit code: 1 (variant only on vulnerable; not a bypass).

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 (show files.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.txthttp://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.2 9491f32a6d459a4d499c5028d37c0d0386771e9f.
  • 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

  1. 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 the ICON slot entirely — defense in depth on top of the serving-layer fix.
  2. Propagate the safety-net CSP/nosniff/X-Frame-Options to cloud objects for AWS S3 (use PutObjectRequest response-header overrides or a CloudFront response-headers policy), Azure, and GCS, so the CSP safety net survives the 302 redirect to the cloud origin.
  3. Add Content-Disposition: attachment to the passthrough types (application/json, application/xml) in createFileResponseHeaders so the only remaining inline-serving branch is the forced text/plain.
  4. 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 (the trap cleanup EXIT removes openvsx-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/-/publish API 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-FileResource sink was rewired to createFileResponseHeaders), 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.

Event 1/40
0:002:27
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-13323 · REPRO-20
0:03
0:05
web search
0:06
web search
0:09
0:10
web search
0:12
web search
0:14
0:16
web search
0:17
0:21
0:22
0:23
web search
0:26
0:27
web search
0:28
0:33
0:34
0:35
web search
0:44
0:46
0:47
web search
0:59
1:00
web search
1:28
1:30
1:49
1:50
web search
1:57
2:27

Artifacts and Evidence for CVE-2026-4983

Scripts, logs, diffs, and output captured during the reproduction.

bundle/ticket.md1.0 KB
bundle/ticket.json1.4 KB
bundle/logs/vuln_v1.0.1_headers.txt0.3 KB
bundle/logs/vuln_v1.0.1_body.html0.6 KB
bundle/logs/fixed_v1.0.2_headers.txt0.4 KB
bundle/logs/fixed_v1.0.2_body.html0.6 KB
bundle/logs/header_comparison.txt1.0 KB
bundle/logs/vuln_analysis.json0.1 KB
bundle/logs/fixed_analysis.json0.2 KB
bundle/logs/reproduction_verdict.txt0.4 KB
bundle/logs/server_vuln_v1.0.1.log37.8 KB
bundle/logs/server_fixed_v1.0.2.log37.8 KB
bundle/logs/reproduction_steps.log15.6 KB
bundle/logs/create_vsix.py3.1 KB
bundle/logs/application.yml2.1 KB
bundle/logs/build_v1.0.1.log2.8 KB
bundle/logs/build_v1.0.2.log2.8 KB
bundle/repro/validation_verdict.json0.9 KB
bundle/repro/runtime_manifest.json1.0 KB
bundle/testpub.testext-1.0.0.vsix1.9 KB
bundle/logs/vuln_variant/variant_repro.log33.2 KB
bundle/logs/vuln_variant/server_vuln_v1.0.1.log41.4 KB
bundle/logs/vuln_variant/vuln_publish.json1.4 KB
bundle/logs/vuln_variant/vuln_variant_headers.txt0.3 KB
bundle/logs/vuln_variant/vuln_variant_body.html0.7 KB
bundle/logs/vuln_variant/vuln_control_headers.txt0.3 KB
bundle/logs/vuln_variant/vuln_control_body.html0.7 KB
bundle/logs/vuln_variant/vuln_metadata.json1.5 KB
bundle/logs/vuln_variant/vuln_icon_url.txt0.1 KB
bundle/logs/vuln_variant/server_fixed_v1.0.2.log37.8 KB
bundle/logs/vuln_variant/fixed_publish.json1.4 KB
bundle/logs/vuln_variant/fixed_variant_headers.txt0.4 KB
bundle/logs/vuln_variant/fixed_variant_body.html0.7 KB
bundle/logs/vuln_variant/fixed_control_headers.txt0.4 KB
bundle/logs/vuln_variant/fixed_control_body.html0.7 KB
bundle/logs/vuln_variant/fixed_metadata.json1.5 KB
bundle/logs/vuln_variant/fixed_icon_url.txt0.1 KB
bundle/logs/vuln_variant/variant_verdict.txt0.7 KB
bundle/logs/vuln_variant/variant_result.json0.5 KB
bundle/logs/vuln_variant/fixed_version.txt0.2 KB
bundle/logs/vuln_variant/latest_version.txt0.2 KB
bundle/vuln_variant/patch_analysis.md11.4 KB
bundle/vuln_variant/create_variant_vsix.py3.4 KB
bundle/vuln_variant/vpub.vext-1.0.0.vsix2.0 KB
bundle/vuln_variant/application.yml2.1 KB
bundle/vuln_variant/write_verdict.py1.1 KB
bundle/vuln_variant/variant_manifest.json6.7 KB
bundle/vuln_variant/validation_verdict.json3.3 KB
bundle/vuln_variant/source_identity.json1.8 KB
bundle/vuln_variant/runtime_manifest.json3.4 KB
bundle/vuln_variant/root_cause_equivalence.json3.9 KB
bundle/repro/reproduction_steps.sh21.7 KB
bundle/repro/rca_report.md10.9 KB
bundle/vuln_variant/reproduction_steps.sh20.8 KB
bundle/vuln_variant/rca_report.md17.4 KB
08 · How to Fix

How to Fix CVE-2026-4983

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-4983 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-4983

How does the CVE-2026-4983 stored XSS attack work?

An attacker registers a publisher account, uploads a VSIX package containing a crafted HTML or SVG file with embedded JavaScript, and induces an authenticated user to visit the resulting /vscode/unpkg/ URL. Because the response lacks CSP and attachment headers, the browser renders the payload inline in the open-vsx.org origin context, allowing the script to exfiltrate session cookies/tokens, generate persistent Personal Access Tokens, and publish malicious extension versions under the victim's identity.

How severe is CVE-2026-4983?

It is rated medium severity. The impact includes session/token exfiltration, persistent PAT generation, and unauthorized publication of malicious extension versions, which also poses a supply-chain risk since Open VSX extensions are distributed to VS Code, VSCodium, Cursor, Windsurf, and compatible editors.

How can I reproduce CVE-2026-4983?

Download the verified script from this page and run it in an isolated environment against an affected Open VSX Registry instance (versions before 1.0.2). It registers a publisher, uploads a VSIX containing a crafted HTML payload, requests the resulting /vscode/unpkg/ URL, and shows the response served as inline text/html without a CSP or attachment header, then confirms the fixed version serves it safely.
11 · References

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.