The mariowOS kernel was lacking authentication across almost its entire API surface and was listening on all network interfaces. This combination meant that a device on the same local network can control system functions on the user's PC without credentials. Multiple endpoints allow command execution; the existing access controls can be bypassed.
| Severity | Count |
|---|---|
| Critical | 6 |
| High | 7 |
| Medium | 6 |
| Low | 3 |
/api/recovery/debug without passwordsystem/server.js:982-998
The route passes req.body.command directly to exec(). The only check is the config.allowFlashing flag; no password is required. Since the flag can be modified by another unprotected route (see C-03) and defaults to true in the distributed config.json, this check offers no real protection.
allowFlashing to false. Unrestricted command execution via HTTP should not exist in a production build./api/remote/execsystem/server.js:2615-2631
The check is if (!match && currentConfig.passwordHash). When passwordHash is null/empty (first boot, after a factory reset, or after /clear-password), the condition evaluates to false and the command is executed without verification.
passwordHash β 403, never execute. Use constant-time comparison and rate-limiting on attempts. Ideally, gate this behind an authenticated session as well./save-settingssystem/server.js:1084-1103
save-settings copies every key not present in the PROTECTED list (passwordHash, verified, quickSettings) into the config. Security flags like allowFlashing, frpLock, rules, and installedApps are not protected and can be set by an unauthenticated request.
allowFlashing for C-01)./api/system/frp-unlocksystem/server.js:957-977
With type:"file", the route executes config = { ...payload, frpLock:false } and overwrites config.json from unauthenticated input: this allows injecting an arbitrary passwordHash and bypassing the FRP lock.
passwordHash.system/server.js:1136+ (/api/system/factory-reset) and :938 (/api/recovery/wipe)
Both routes delete files and clear the config without any authentication. Aside from direct damage (data loss), they bring the state to "no password," reopening C-02.
passwordHash of a currently active user.system/server.js:1434-1470 (/api/sandbox/upload, /api/sandbox/start)
The vmId from the request is concatenated into paths (path.join(sandboxBaseDir, vmId, "disk")) without validation. Then, /api/sandbox/start writes a wrapper and executes node in the resulting directory.
vmId with a strict allowlist (^[a-zA-Z0-9_-]+$), resolve the path, and verify it is contained within sandboxBaseDir (similar to what is done for /home in resolveVirtualPath).system/server.js:2887 β app.listen(PORT) lacks a host parameter (though the log prints 127.0.0.1).
app.listen(PORT, '127.0.0.1'), allowing intentional overrides via a documented environment variable.All /api/* routes except /api/remote/exec lack any verification; /login only returns the desktop HTML and does not establish a session.
hasValidShutdownToken (:147) already exists as a baseline to generalize./deviceresolveDevicePath :2655 + GET /api/system/files/content
/device exposes the entire host filesystem for reading via HTTP; sensitive user files (keys, configs, saved credentials) can be read by a network caller.
/home; for /device, retain only directory listing capabilities, or apply a denylist and fs.realpathSync before containment checks (also against symlinks/junctions).:594/790 (Wi-Fi), :831/886 (Bluetooth), :1837 (extractCmd), :491 (volume)
Request inputs (SSID, MAC, device name, zip path) are interpolated into strings passed to exec().
exec(string) with the spawn(command, args) wrapper already present at :25 (arguments as arrays, no shell); validate inputs with allowlist regexes.Access-Control-Allow-Origin: * on update streams:2392, :2464 β Any website opened by the user can interact with these streams.
:1729 case 'run': exec(params.command) β Rules are saved via /api/rules/save (:1673) without authentication and triggered by node-cron.
/api/rules/save.:1454 git clone of arbitrary URLs, :1296/1318 git pull, OTA/SOTA updates, respawn :2300
Remote code and zip files are downloaded and executed without signature or checksum verification.
nodemailer (SMTP command injection, GHSA-c7w3-x93f-qmm8 / GHSA-vvjj-xcjg-gr5g) in both package.json files. Fix: Update to β₯ 8.0.4.node-cron 3.0.x (vulnerable uuid dependency). Fix: Update to 4.x.body-parser 2.0β2.2.2 (DoS, GHSA-v422-hmwv-36x6). Fix: Update dependency.:443, :462, :499, restore :897). Fix: Apply limits.fileSize, validate names with path.basename, verify file signatures./api/system/restore rebuilds the config from an unauthenticated uploaded file (:906). Fix: Authenticate and validate the schema.eval() used in the frontend terminal desktop/apps/terminal/index.html:1437. Fix: Review this; if processing untrusted input, replace with an explicit parser.launcher.js:328 webviewTag:true without will-attach-webview / setWindowOpenHandler event handlers. Fix: Restrict navigation and attachment of child webviews.@tomphttp/bare-server-node is declared but unused. Fix: Remove the dependency.system/server.js unless noted):127.0.0.1 (override via MARIOWOS_BIND_HOST)./api/recovery/debug now requires flag and password; allowFlashing:false in config.json./api/remote/exec fails closed if no password is configured./save-settings converted to an allowlist model.frp-unlock can no longer inject an arbitrary passwordHash.vmId validated during sandbox upload/start/delete./home (no exfiltration from /device).connect-wifi and connect-bt.run action of the rules engine disabled.nodemailerβ10.x, node-cronβ4.x, body-parser updated. npm audit = 0 vulnerabilities in both folders.keys.json removed from tracking. Urgent action on the SMTP password in history is still pending (see below).HttpOnly; SameSite=Strict, issued at login/setup, required on every mutating request (POST/PUT/DELETE/PATCH) except the login/recovery flow. Secret stored in system/session.key (gitignored, never in config.json). Tested end-to-end: unauthenticated POST β 401, valid login β cookie + 200, authenticated call β 200. New endpoint /api/system/logout.
SameSite=Strict, this also closes the CSRF vector from local web pages targeting 127.0.0.1 on destructive routes: C-05 (factory-reset/wipe) and M-05 (/api/system/restore) are now protected.version.json. install registers repoUrl + version; check-upgrades/do-upgrade compare the local version with the one declared in the app repo's version.json (semver rules, isNewerVersion helper) and only update forward via git pull --ff-only from the set origin. repoUrl is constrained to https github.com URLs (isAllowedRepoUrl) β this also hardens git clone against malicious URLs.system/app-permissions.json (prefixβapp rules), per-app signed token via POST /api/app/token, enforceAppPermissions middleware. Disabled by default (enforced:false) to avoid breaking apps until the frontend sends X-App-Token. Tested: no tokenβ403, valid tokenβ200, app token on another app's endpointβ403. Honest limitation: Apps are same-origin iframes β least-privilege by convention and traceability, not strong isolation (would require separate per-app origins).verifyUpdateAuthenticity() before downloading/applying: it verifies a detached signature of version.json against a local public key. Opt-in by key presence: Depositing system/update-pubkey.pem and publishing system/version.json.sig (base64 signature) in the repo makes updates authenticated, and unsigned ones are rejected; without the key, behavior is unchanged with a warning. git clone/git pull of store apps are also constrained to https github.com URLs. The SOTA chain already had integrity verification (hash vs manifest). Note: The signature protects against third-parties/MITM and tampered sources, not against a first-party maintainer signing with the official key.eval. math command now uses a safe arithmetic parser (safeMath, shunting-yard); tested to block arbitrary JS.launcher.js: web-contents-created with will-attach-webview (denies Node/preload in child webviews) and setWindowOpenHandler (http links β external browser, all else denied).GET /api/recovery/verify-integrity: compares system/ files with a signed manifest (integrity-manifest.json + .sig, verified against update-pubkey.pem). Reports modified/missing files and, if the system is tampered and is locked (allowFlashing off), returns recommendReflash:true β the intended flow: system touched without unlock β recovery flags it and prompts to reflash clean stock. Real backing for the "VERIFYING FILESYSTEM INTEGRITY" screen, previously a fake loader. Added tool system/tools/gen-integrity-manifest.js to generate the manifest on release. Tested: intact build β tampered:false; altered file β tampered:true + recommendReflash:true.POST /api/recovery/set-flashing requiring mandatory account password: the only way to enable allowFlashing, now that C-03 removed the remote path. Thus, "unlock" is a deliberate act by the local user, not a flag secretly flipped by whoever delivers the build. Tested: no session β 401, wrong password β 403, correct β unlocks with explicit warning.