Advisory Details
Title: LobsterAI HTML preview server follows in-root symlinks and discloses arbitrary local files
Description:
Summary
LobsterAI's localhost HTML preview server tries to confine preview asset requests to the selected preview directory with a lexical path.resolve(...).startsWith(...) check, but it then serves the resulting path with normal symlink-following filesystem calls. If a previewed HTML artifact directory contains a child symlink that points outside the intended preview root, the preview server will return files from the external target to the preview page.
Details
I verified this against LobsterAI's real src/main/libs/htmlPreviewServer.ts implementation and the real loopback HTTP server started by createPreviewSession(). This is not a generic ../ traversal finding. The server correctly rejects encoded traversal such as ..%2Foutside%2Fcanary.txt with HTTP 403. The escape happens because the confinement check is lexical, not canonical.
The relevant logic is:
const resolvedPath = path.resolve(session.rootDir, relativePath);
if (!resolvedPath.startsWith(session.rootDir)) {
res.writeHead(403);
res.end('Forbidden');
return;
}
streamFile(resolvedPath, res);
Once resolvedPath passes the prefix check, streamFile() uses fs.stat() and fs.createReadStream() on that path. Those calls follow symlinks, so a path that still looks in-root lexically can resolve to an external file on disk at open time:
fs.stat(filePath, (err, stat) => {
...
const stream = fs.createReadStream(filePath);
stream.pipe(res);
});
In the attached reproduction, the previewed HTML file lives under a temporary workspace/ directory. A sibling symlink workspace/escape -> ../outside/ is created before the preview request is made. The direct traversal control request returns 403, and the same escape/canary.txt child path returns 404 when the symlink is absent. After the symlink is present, the same child path returns 200 and serves the canary from the external outside/ directory.
I also checked the public release history before assigning the affected range. Release 2026.5.12 does not contain src/main/libs/htmlPreviewServer.ts at all. Release 2026.5.14 introduces the preview server with the same vulnerable lexical containment logic, and the highest public release 2026.6.15 still contains the bug. That release tag resolves to remote commit 5351d1ec650325b6a107a99704a1e5415de9097d, and all occurrence links below are pinned to that exact commit.
This reproduction uses the real preview server module and real HTTP requests over 127.0.0.1. A full Electron UI run was not required to confirm the defect, although in this workspace npm run electron:dev also failed earlier because the local host did not meet the repository's Node/Electron runtime requirements.
PoC
Prerequisites
- A checkout of
https://github.com/netease-youdao/LobsterAI
python3
node with --experimental-transform-types support; the repository itself requires Node >=24 <25
- Install repository dependencies first so
src/main/libs/htmlPreviewServer.ts and its imports can be executed from the source tree
- Place the downloaded PoC files into
llm-enhance/cve-finding/similar/Info_Leak/Advisory-GHSA-qcc4-p59m-p54m-html-preview-symlink-escape-exp/ inside the repository checkout so their relative paths resolve correctly
Reproduction Steps
- Download the harness from: preview_session_harness.mjs
- Download the verification driver from: verification_test.py
- Download the control driver from: control-normal-behavior.py
- Place the three files under
llm-enhance/cve-finding/similar/Info_Leak/Advisory-GHSA-qcc4-p59m-p54m-html-preview-symlink-escape-exp/ in the LobsterAI checkout.
- From the repository root, run the verification case:
python3 llm-enhance/cve-finding/similar/Info_Leak/Advisory-GHSA-qcc4-p59m-p54m-html-preview-symlink-escape-exp/verification_test.py
- Confirm the output shows:
- encoded
../ traversal returns HTTP 403
- the symlinked child request returns
HTTP 200
- the outside canary file is disclosed
- Run the matched control case:
python3 llm-enhance/cve-finding/similar/Info_Leak/Advisory-GHSA-qcc4-p59m-p54m-html-preview-symlink-escape-exp/control-normal-behavior.py
- Confirm the control output shows:
- encoded
../ traversal still returns HTTP 403
- the same
escape/canary.txt child path returns HTTP 404 when no symlink is present
Log of Evidence
Observed verification output:
[Verification Mode] Integration-Test
[Setup] Launching the real preview HTTP server through createPreviewSession() and exercising it over localhost.
[Liveness] Preview server bound to 127.0.0.1:46739
[Control] Encoded ../ traversal returned HTTP 403
[Exploit] Symlinked child path returned HTTP 200
[DEFECT-CONFIRMED-WITH-LIMITATIONS]
[Evidence] Canary leaked via http://127.0.0.1:46739/6b4464f1f02008e8dd41ba20452479c1/escape/canary.txt?token=ce3aef561201791e1b0e7978a79e030f68f28d0cf5a9a4b1
Observed control output:
[Verification Mode] Integration-Test
[Setup] Launching the same preview server without the symlink trigger input.
[Liveness] Preview server bound to 127.0.0.1:38599
[Control] Encoded ../ traversal returned HTTP 403
[Baseline] Same child path without symlink returned HTTP 404
[CONTROL-PASS]
Independent raw HTTP evidence:
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, must-revalidate
Access-Control-Allow-Origin: *
Content-Type: text/plain; charset=utf-8
lobster-canary-1781545169094-72f560e0e614
Impact
This is a local file disclosure issue in LobsterAI's HTML artifact preview path. An attacker-controlled or attacker-influenced file-based HTML artifact can cause the preview server to serve files outside the intended preview directory as long as the preview root contains an in-root symlink alias to an external location.
The impacted asset is any local file readable by the LobsterAI desktop process under the victim user's account, including source files, tokens stored in plain-text config files, local documents, and other workspace material outside the directory the user expected to preview. Because the disclosure happens through the preview origin itself, the previewed page can request those bytes as relative assets once the user opens the artifact.
Affected products
- Ecosystem: npm
- Package name: lobsterai
- Affected versions: >= 2026.5.14, <= 2026.6.15
- Patched versions:
Severity
- Severity: Medium
- Vector string: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
Weaknesses
- CWE: CWE-59: Improper Link Resolution Before File Access ('Link Following')
Occurrences
| Permalink |
Description |
|
const resolvedPath = path.resolve(session.rootDir, relativePath); |
|
|
|
// Path traversal protection |
|
if (!resolvedPath.startsWith(session.rootDir)) { |
|
res.writeHead(403); |
|
res.end('Forbidden'); |
|
return; |
|
} |
|
|
|
streamFile(resolvedPath, res); |
|
handleRequest() resolves the attacker-influenced child path under session.rootDir and only enforces lexical prefix containment with startsWith, which misses symlink aliases that still retain the same path prefix. |
|
function streamFile(filePath: string, res: http.ServerResponse): void { |
|
fs.stat(filePath, (err, stat) => { |
|
if (err || !stat.isFile()) { |
|
res.writeHead(404); |
|
res.end('Not Found'); |
|
return; |
|
} |
|
|
|
writePreviewHeaders(res, 200, { |
|
'Content-Type': getMimeType(filePath), |
|
'Content-Length': stat.size, |
|
}); |
|
|
|
const stream = fs.createReadStream(filePath); |
|
stream.pipe(res); |
|
streamFile() uses fs.stat() and fs.createReadStream() on the already-approved path. Those filesystem calls follow the symlink and stream the external target file back to the preview client. |
|
export async function createPreviewSession(filePath: string): Promise<{ sessionId: string; url: string }> { |
|
const resolvedFilePath = path.resolve(filePath); |
|
const stat = await fs.promises.stat(resolvedFilePath); |
|
if (!stat.isFile()) { |
|
throw new Error('Preview target is not a file'); |
|
} |
|
|
|
const port = await startHtmlPreviewServer(); |
|
const sessionId = crypto.randomBytes(16).toString('hex'); |
|
const token = crypto.randomBytes(24).toString('hex'); |
|
const rootDir = path.dirname(resolvedFilePath) + path.sep; |
|
const fileName = path.basename(resolvedFilePath); |
|
|
|
sessions.set(sessionId, { rootDir, token, filePath: resolvedFilePath, kind: 'html' }); |
|
|
|
const url = `http://127.0.0.1:${port}/${sessionId}/${encodeURIComponent(fileName)}?token=${token}`; |
|
createPreviewSession() anchors the trust boundary to path.dirname(resolvedFilePath) + path.sep and exposes the selected HTML file over the localhost preview server, making sibling asset requests reachable through the same session. |
Advisory Details
Title: LobsterAI HTML preview server follows in-root symlinks and discloses arbitrary local files
Description:
Summary
LobsterAI's localhost HTML preview server tries to confine preview asset requests to the selected preview directory with a lexical
path.resolve(...).startsWith(...)check, but it then serves the resulting path with normal symlink-following filesystem calls. If a previewed HTML artifact directory contains a child symlink that points outside the intended preview root, the preview server will return files from the external target to the preview page.Details
I verified this against LobsterAI's real
src/main/libs/htmlPreviewServer.tsimplementation and the real loopback HTTP server started bycreatePreviewSession(). This is not a generic../traversal finding. The server correctly rejects encoded traversal such as..%2Foutside%2Fcanary.txtwith HTTP403. The escape happens because the confinement check is lexical, not canonical.The relevant logic is:
Once
resolvedPathpasses the prefix check,streamFile()usesfs.stat()andfs.createReadStream()on that path. Those calls follow symlinks, so a path that still looks in-root lexically can resolve to an external file on disk at open time:In the attached reproduction, the previewed HTML file lives under a temporary
workspace/directory. A sibling symlinkworkspace/escape -> ../outside/is created before the preview request is made. The direct traversal control request returns403, and the sameescape/canary.txtchild path returns404when the symlink is absent. After the symlink is present, the same child path returns200and serves the canary from the externaloutside/directory.I also checked the public release history before assigning the affected range. Release
2026.5.12does not containsrc/main/libs/htmlPreviewServer.tsat all. Release2026.5.14introduces the preview server with the same vulnerable lexical containment logic, and the highest public release2026.6.15still contains the bug. That release tag resolves to remote commit5351d1ec650325b6a107a99704a1e5415de9097d, and all occurrence links below are pinned to that exact commit.This reproduction uses the real preview server module and real HTTP requests over
127.0.0.1. A full Electron UI run was not required to confirm the defect, although in this workspacenpm run electron:devalso failed earlier because the local host did not meet the repository's Node/Electron runtime requirements.PoC
Prerequisites
https://github.com/netease-youdao/LobsterAIpython3nodewith--experimental-transform-typessupport; the repository itself requires Node>=24 <25src/main/libs/htmlPreviewServer.tsand its imports can be executed from the source treellm-enhance/cve-finding/similar/Info_Leak/Advisory-GHSA-qcc4-p59m-p54m-html-preview-symlink-escape-exp/inside the repository checkout so their relative paths resolve correctlyReproduction Steps
llm-enhance/cve-finding/similar/Info_Leak/Advisory-GHSA-qcc4-p59m-p54m-html-preview-symlink-escape-exp/in the LobsterAI checkout.python3 llm-enhance/cve-finding/similar/Info_Leak/Advisory-GHSA-qcc4-p59m-p54m-html-preview-symlink-escape-exp/verification_test.py../traversal returnsHTTP 403HTTP 200python3 llm-enhance/cve-finding/similar/Info_Leak/Advisory-GHSA-qcc4-p59m-p54m-html-preview-symlink-escape-exp/control-normal-behavior.py../traversal still returnsHTTP 403escape/canary.txtchild path returnsHTTP 404when no symlink is presentLog of Evidence
Observed verification output:
Observed control output:
Independent raw HTTP evidence:
Impact
This is a local file disclosure issue in LobsterAI's HTML artifact preview path. An attacker-controlled or attacker-influenced file-based HTML artifact can cause the preview server to serve files outside the intended preview directory as long as the preview root contains an in-root symlink alias to an external location.
The impacted asset is any local file readable by the LobsterAI desktop process under the victim user's account, including source files, tokens stored in plain-text config files, local documents, and other workspace material outside the directory the user expected to preview. Because the disclosure happens through the preview origin itself, the previewed page can request those bytes as relative assets once the user opens the artifact.
Affected products
Severity
Weaknesses
Occurrences
LobsterAI/src/main/libs/htmlPreviewServer.ts
Lines 282 to 291 in 5351d1e
handleRequest()resolves the attacker-influenced child path undersession.rootDirand only enforces lexical prefix containment withstartsWith, which misses symlink aliases that still retain the same path prefix.LobsterAI/src/main/libs/htmlPreviewServer.ts
Lines 96 to 110 in 5351d1e
streamFile()usesfs.stat()andfs.createReadStream()on the already-approved path. Those filesystem calls follow the symlink and stream the external target file back to the preview client.LobsterAI/src/main/libs/htmlPreviewServer.ts
Lines 345 to 360 in 5351d1e
createPreviewSession()anchors the trust boundary topath.dirname(resolvedFilePath) + path.sepand exposes the selected HTML file over the localhost preview server, making sibling asset requests reachable through the same session.