No cloud. Runs locally. Python · stdlib only Whitelist & graylist security

Give your website
local machine power.

web2local is a tiny daemon that bridges websites to your local environment — safely. Your site can run commands, read files, and talk to hardware that a browser alone can never reach.

How it works

Browsers are sandboxed on purpose — a web page cannot touch your filesystem, run a process, or connect to a serial port without explicit permission. But some tools genuinely need local power: build scripts, hardware dashboards, developer utilities, media processors.

web2local runs a small HTTP server bound to 127.0.0.1 only (never reachable from the internet). Your website calls it with fetch(). The daemon checks who is asking, then acts accordingly.

Your websitemysite.com
──→
web2local daemon127.0.0.1:7878
──→
Local machineshell · files · hardware

The daemon answers only to requests whose Host header is 127.0.0.1 or localhost. This one check kills the most dangerous class of localhost attacks — DNS rebinding — where a malicious site manipulates DNS to make the browser treat evil.com as a local address.

Why Host header, not just CORS? CORS controls which pages can read the response; the Host header check prevents the request from being accepted at all if it arrived via a DNS-rebound domain. They defend against different attacks.

Security model

Every request carries an Origin header — the site that made it. The daemon classifies that origin into one of three tiers:

Whitelist
Origins you fully trust. Commands execute immediately with no prompt. Use for your own sites.
⚠️
Graylist
Origins you want to evaluate. A native dialog shows the exact command. You read it, then allow or deny. Auto-denies after 120 s.
🚫
Blocked
Any origin not on either list. Returns HTTP 403 immediately. No command is shown or executed.

What the graylist dialog shows you

When a graylist site sends a command, a window appears with:

The command is displayed as an argument list — no shell expansion happens. What you see is exactly what will run. Scroll right if it is long; the dialog will not dismiss until you click a button.

What it does not protect against

The graylist is only as good as your reading. Keep these risks in mind:

  • Rubber-stamping — users who click Allow without reading are the weakest link.
  • State accumulation — a site approved for 10 harmless commands may use their combined effect maliciously.
  • Non-browser callersOrigin is only enforced by browsers. A script using curl can forge it. The daemon should only be used from browser-based websites.
  • Broad whitelist scope — whitelisting https://example.com trusts every page on that domain.

Quick start

Get the daemon

A single Python file, no dependencies beyond the standard library. Requires Python 3.7+.

Run the daemon

python3 daemon.py

The daemon binds to 127.0.0.1:7878 and stays running in your terminal. On the first launch it creates a config file at ~/.config/web2local/config.json.

Add your site to a list

You can do this from the demo below, from the config file directly, or via the API:

# Whitelist (trust fully)
curl -s -X POST http://127.0.0.1:7878/config/whitelist \
  -H 'Content-Type: application/json' \
  -d '{"origin":"https://mysite.com"}'

# Graylist (ask before each command)
curl -s -X POST http://127.0.0.1:7878/config/graylist \
  -H 'Content-Type: application/json' \
  -d '{"origin":"https://other-site.com"}'

Include the client library

<script src="client.js"></script>

Copy client.js from this repo into your project.

Call the API from your page

const w2l = new Web2Local();   // default port 7878

if (await w2l.isRunning()) {
  const r = await w2l.run("ls", ["-la", "/tmp"]);
  console.log(r.stdout);
  console.log("exit:", r.exit_code);
} else {
  console.warn("web2local daemon not running");
}

API reference

All endpoints are on http://127.0.0.1:7878.

Method & Path Auth Description
GET /status any origin Returns {"status":"running","version":"1.0.0"}. Use this to detect the daemon.
POST /run whitelist / graylist Execute a command. Body: {"command":"ls","args":["-la"]}. Returns {"stdout":"…","stderr":"…","exit_code":0}.
GET /config any (local only) Return current whitelist and graylist.
POST /config/whitelist any (local only) Add origin to whitelist. Body: {"origin":"https://…"}.
POST /config/graylist any (local only) Add origin to graylist.
POST /config/remove any (local only) Remove origin from all lists.
POST /config/reload any (local only) Reload config from disk without restarting.
GET /log any (local only) Return last 200 audit log lines.
POST /spawn whitelist / graylist Start a long-running process. Returns immediately with {"pid":…, "started_at":"…", "log_path":"…"}. Output is captured to the log file.
POST /stop whitelist / graylist Stop a spawned process by PID. Body: {"pid":1234}. Sends SIGTERM, then SIGKILL after 3 s.
GET /ps any (local only) List spawned processes still alive (verified via /proc start-time, so PID reuse won't fool it).
GET /logs?pid=N any (local only) Return the last 200 lines of a spawned process's captured output.
POST /deploy whitelist / graylist Verify SHA-256 → write script to ~/.config/web2local/agents/ → spawn it. Always shows the approval dialog, even for whitelist origins — the dialog shows filename, full SHA-256, destination path, and the exact command. Body: {"source":"…","sha256":"…","filename":"…","command":"python3","args":["serve",…]}. Returns {"pid":…,"path":"…","already_running"?:true}.

Error responses

403  {"error": "origin not in whitelist or graylist"}
403  {"error": "command denied by user"}
403  {"error": "host must be 127.0.0.1 or localhost"}
400  {"error": "command must be a non-empty string"}
408  {"error": "command timed out (30 s limit)"}

Command rules

Live demo

This page connects to your local daemon in real time.

Checking for daemon at 127.0.0.1:7878…

Run a command

Output will appear here.

Manage lists

✅ Whitelist
Loading…
⚠️ Graylist
Loading…

Audit log (last 20 entries)

Loading…

Process manager

POST /run waits for the command to finish — fine for quick jobs, useless for daemons. POST /spawn starts a process and returns immediately with its PID. The daemon tracks every spawned PID, captures its output to a log file, and lets you stop it from this page.

Spawn a process

Tip: for a shell pipeline use sh with -c "…". Output is captured to ~/.config/web2local/processes/.

Running processes

Loading…
Process output (last 200 lines)