MeghXL is a small, self-hosted, LAN-first file-transfer tool. This document explains its security model honestly so you can decide whether to trust it — and, more importantly, so you can verify it yourself. Nothing here asks you to take our word for it; every claim below points at code you can read in minutes.
Please report security issues privately — do not open a public issue for a vulnerability.
- Preferred: GitHub → the repo's Security tab → “Report a vulnerability” (private security advisory).
- Email:
riponce.buet [at] gmail [dot] com
We aim to acknowledge reports within a few days. Responsible disclosure is appreciated; we will credit reporters who want credit.
| Version | Supported |
|---|---|
| 1.x (latest) | ✅ |
| < 1.0 | ❌ |
Security fixes land on the latest 1.x release (and main). Please run a
recent version.
MeghXL is deliberately built to be auditable in one sitting:
- No build step, no minification, no obfuscation. What you read is what runs.
The frontend is plain HTML/CSS/JS in
public/; the server is plain Node inserver.js+src/. - No telemetry, analytics, tracking, or “phone-home”. Nothing about you, your files, your devices, or your usage is ever sent anywhere. The server serves your files on your network and (optionally) advertises an mDNS name on the LAN.
- Exactly one outbound request exists, and only on demand. Pressing
Check for updates (About page, or the desktop app's tray) performs a single
GETto the GitHub releases API to compare version numbers. It sends no identifiers, no file names, no usage data — nothing but the request itself, and the answer is cached for an hour. Never on a timer, at launch, or in the background. Other websites can't trigger it (cross-site requests are refused); a script on your LAN could, since the endpoint needs no login. The code is one short file,src/routes/update.js; delete that route if you want it gone — nothing else depends on it. - Desktop updates are signature-verified. The app only installs a package signed with the project's private key, checked against the public key compiled into the bundle. A tampered or unsigned package is refused, so a compromised download host cannot push you malicious code.
- No dynamic code execution. No
eval, nonew Function, nochild_processat runtime. (The only shell scripts in the repo are the opt-in auto-start helpers underscripts/, which run only when you explicitly callnpm run autostart:install.) - No install-time scripts.
package.jsonhas nopostinstall/preinstallhooks —npm installwon't run arbitrary code. - Minimal dependencies: 7 well-known runtime packages
(
express,ws,multer,qrcode,qrcode-terminal,mime-types,bonjour-service), pinned viapackage-lock.json.npm auditreports 0 known vulnerabilities at release. - Apache-2.0 licensed, all history public.
git clone https://github.com/riponcm/MeghXL.git && cd MeghXL
npm ci # installs exactly what package-lock.json pins
npm audit # expect: 0 vulnerabilities
npm test # the included test suite
# Spot-check the claims above:
grep -rn "eval(\|new Function\|child_process" src server.js # (nothing)
grep -rni "analytics\|telemetry\|track" src public # (nothing)
node -e "console.log(require('./package.json').scripts)" # no postinstallRead server.js and the files in src/ — the whole server is a few hundred lines.
MeghXL assumes the local network is trusted. That is the security boundary.
- On your LAN, anyone who can reach the URL can see/download public files, post shared-clipboard notes, and upload — that's the point of a same-network sharing tool.
- MeghXL never exposes itself to the internet. There is no built-in tunnel and
no “go public” button. It only listens on the port you bind, on your own
network. To reach it remotely you must put it behind infrastructure you
control (a VPN such as WireGuard/Tailscale, or an authenticated reverse proxy)
and set
PUBLIC_BASE_URL.
| Risk | Mitigation | Where |
|---|---|---|
| Path traversal | Client filenames never touch disk; files are stored under server-generated random names and served only by token, with a path-containment check. | src/routes/files.js (storedPath), src/ids.js |
| Guessing private links | Tokens are 128-bit, URL-safe random strings (crypto.randomBytes(16)). |
src/ids.js |
| Another LAN device becoming admin | The host console is granted only to a connection from the host machine itself (no forwarding header), an ADMIN_IP, or the ADMIN_KEY header. A spoofed X-Forwarded-For cannot grant admin, and the host must be named by an address or its own name, so a device answering mDNS for some other .local name can't borrow it. |
src/admin-auth.js, src/request-guard.js (+ tests) |
| Websites attacking the hub through the host's browser (DNS rebinding, CSRF, cross-site WebSockets, clickjacking) | Because any web page open on the host PC can make its browser connect from loopback, the machine check alone is not enough. Every request is also checked: unknown Host names are refused (rebinding), writes must come from a MeghXL page (Origin / Sec-Fetch-Site), admin writes need a header only MeghXL's own script sends, WebSocket handshakes must be same-origin, and no page can be framed. Fixed in 1.0.1 — see the advisory below. |
src/request-guard.js, src/admin-auth.js (+ tests) |
| XSS / HTML injection | All user-supplied content (filenames, notes, device names) is inserted with textContent. innerHTML is used only for the project's own static SVG icon markup — never for user input. |
public/app.js, public/admin.js |
| Uploaded files running as pages | Downloads are always Content-Disposition: attachment with nosniff, and carry Content-Security-Policy: sandbox and Cross-Origin-Resource-Policy: same-origin — so an uploaded .html or .svg can't run on MeghXL's origin or be embedded by another site. |
src/routes/files.js |
| HTTP header injection | Content-Disposition filenames are sanitized (control chars stripped, RFC 5987 encoded). |
src/routes/files.js |
| Memory exhaustion | Uploads stream to disk (never buffered in RAM); upload size can be capped with MAX_UPLOAD_MB (unlimited by default); JSON bodies capped at 16 KB. |
src/uploads.js, server.js |
Honesty matters more than reassurance:
- On the LAN, public files can be read and deleted by anyone on the network (the “shared board” model). Use private + expiry + one-time links for anything sensitive.
- No at-rest encryption of stored files.
- No rate limiting / brute-force throttling. This is acceptable on a trusted LAN (and private tokens are 128-bit), but it is not hardened to face a hostile network directly.
- It is not designed to be directly internet-facing without your own authentication layer in front (see below).
- Admin is tied to the host machine, not to a login. Every browser on the host
PC — and every program and OS user on it — has admin rights. Don't run MeghXL on
a PC shared with people you don't trust. A reverse proxy on the same machine must
send
X-Forwarded-For, or the people it forwards look like the host. A per-install pairing secret is planned for 1.1. - Device identity is not authenticated between LAN devices. A device on your network can claim another device's id and receive its direct sends and private messages, and sender names on notes and messages are self-declared. Only the announcement label ("Announced by Admin (host)") is reserved to the admin.
- Host names. IP addresses,
localhost, and local names (.local,.lan, single-label names, …) work out of the box. Any other name — a proxy's public domain, a Tailscale*.ts.netname — must be listed inALLOWED_HOSTSor be the host inPUBLIC_BASE_URL.
- Put it behind a VPN or an authenticated reverse proxy; never port-forward it raw.
- A random admin key is generated on first run and printed in the terminal;
set
ADMIN_KEYto choose your own. Enter it on the console's unlock form rather than in a URL. Scripts send it as thex-admin-keyheader (admin writes also needx-meghxl-request: 1). - Behind a reverse proxy, preserve the
Hostheader and setPUBLIC_BASE_URL(orALLOWED_HOSTS) to the public name, so rebinding protection can see the real host name. - Prefer private + expiry + one-time links for anything sensitive.
- Keep dependencies current (
npm audit, Dependabot), and run a recent Node LTS.
| Version | Issue | Credit |
|---|---|---|
| 1.0.1 (GHSA-q5qg-gwfw-p5w5) | A website open in a browser on the host PC could act as the host console — read and delete files, post announcements, block devices — via CSRF, DNS rebinding or cross-site WebSockets. Fixed by validating Host, Origin and framing on every request. |
kta1kri |
Published advisories: https://github.com/riponcm/MeghXL/security/advisories
MeghXL is open-source (Apache-2.0). If something here doesn't match the code, that's a bug in the docs — please report it.