Skip to content

Security: riponcm/MeghXL

SECURITY.md

Security Policy

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.


Reporting a vulnerability

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.

Supported versions

Version Supported
1.x (latest) ✅
< 1.0 ❌

Security fixes land on the latest 1.x release (and main). Please run a recent version.


Why you can trust the source (and how to confirm it)

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 in server.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 GET to 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, no new Function, no child_process at runtime. (The only shell scripts in the repo are the opt-in auto-start helpers under scripts/, which run only when you explicitly call npm run autostart:install.)
  • No install-time scripts. package.json has no postinstall/preinstall hooks — npm install won't run arbitrary code.
  • Minimal dependencies: 7 well-known runtime packages (express, ws, multer, qrcode, qrcode-terminal, mime-types, bonjour-service), pinned via package-lock.json. npm audit reports 0 known vulnerabilities at release.
  • Apache-2.0 licensed, all history public.

Audit it yourself (~10 minutes)

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 postinstall

Read server.js and the files in src/ — the whole server is a few hundred lines.


Threat model & trust assumptions

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.

What MeghXL protects against

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

What MeghXL does not do (limitations, stated plainly)

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.net name — must be listed in ALLOWED_HOSTS or be the host in PUBLIC_BASE_URL.

Hardening checklist (if you expose it beyond a trusted LAN)

  • 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_KEY to choose your own. Enter it on the console's unlock form rather than in a URL. Scripts send it as the x-admin-key header (admin writes also need x-meghxl-request: 1).
  • Behind a reverse proxy, preserve the Host header and set PUBLIC_BASE_URL (or ALLOWED_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.

Security advisories

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.

Learn more about advisories related to riponcm/MeghXL in the GitHub Advisory Database