fix(server): validate Host header on /argv to prevent DNS rebinding RCE - #1077
Open
sebastionoss wants to merge 1 commit into
Open
fix(server): validate Host header on /argv to prevent DNS rebinding RCE#1077sebastionoss wants to merge 1 commit into
sebastionoss wants to merge 1 commit into
Conversation
The /argv endpoint invokes CmdServer.execute which allows calling arbitrary functions on any registered Python/QML module via --call_py/--call_qml/--func. It was previously guarded only by a REMOTE_ADDR == 127.0.0.1 check, which is bypassable by a malicious website through DNS rebinding: the victim's browser resolves an attacker domain to 127.0.0.1 and issues cross-origin POSTs (permitted by the wildcard CORS headers), granting arbitrary local code execution. Add a Host header allowlist (127.0.0.1 / localhost / ::1). The bundled cmd_client uses urllib against http://127.0.0.1:PORT so its Host header remains valid.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The local HTTP server exposes
POST /argv, which forwards its JSON body toCmdServer.execute()(UmiOCR-data/py_src/server/cmd_server.py:455).execute()parses the payload with argparse and honors--call_py <module> --func <name>/--call_qml <module> --func <name>, invoking arbitrary functions on registered Python/QML modules. Successful exploitation results in local code execution in the context of the Umi-OCR process.The existing guard only checks
request.environ["REMOTE_ADDR"] == "127.0.0.1". That check is bypassable by DNS rebinding: an attacker-controlled domain with a short TTL first resolves to a public IP (to pass the browser's same-origin fetch), then to127.0.0.1. The browser then issues the maliciousPOST /argvfrom the attacker's page; the TCP peer address is genuinely127.0.0.1, soREMOTE_ADDRpasses, but the request originated from a remote web page.CORS_ALLOW_ALLis set on the server, which further removes any cross-origin friction on the response side.UmiOCR-data/py_src/server/web_server.py—_argv()route handlerFix
Add a Host-header allowlist — the standard defense against DNS rebinding. After the
REMOTE_ADDRcheck,/argvnow also requiresHostto be one of127.0.0.1,localhost, or::1(port stripped, IPv6 brackets stripped, case-insensitive). A missingHostheader is rejected conservatively.The bundled CLI client (
cmd_client.py) usesurllibagainsthttp://127.0.0.1:<port>/argv, sourllibsetsHost: 127.0.0.1:<port>automatically — legitimate local usage is unaffected. Browser attacks via a rebound domain sendHost: attacker.com, which is now rejected with HTTP 403.Diff is minimal: one new helper (
_isLoopbackHostHeader) plus a single additional check in_argv. No behavior change for any other route.Testing
urllib.request.Request("http://127.0.0.1:PORT/argv", ...)setsHost: 127.0.0.1:PORT, which passes the new check.127.0.0.1:1234,localhost:1234,[::1]:1234, barelocalhost, mixed case, and empty/missing.POST /argvwithHost: evil.example— server now returns HTTP 403 instead of executing the payload.Umi-OCR --call_py ...from the CLI) is unchanged.Proof of Concept
With Umi-OCR running locally (HTTP server enabled), a request from a browser after a DNS rebind — or, equivalently, any request with a non-loopback
Host— reaches the handler:The actual attack sequence in the wild:
http://rebind.attacker.tld(short-TTL A record initially pointing to the attacker's server so the page loads).127.0.0.1and issuesfetch("http://rebind.attacker.tld:1224/argv", {method:"POST", body: JSON.stringify(["--call_py", ...])}).127.0.0.1:1224;REMOTE_ADDRcheck passes;CmdServer.executeruns the attacker-chosen function.Route/function existence verified in this branch:
UmiOCR-data/py_src/server/web_server.py:70definesPOST /argv;UmiOCR-data/py_src/server/cmd_server.py:455definesexecute;--call_py/--call_qmlare registered atcmd_server.py:406–409.Adversarial review
Before submitting we tried to disprove this. We considered whether the
REMOTE_ADDR == "127.0.0.1"check alone was sufficient — it isn't, because DNS rebinding intentionally causes the browser to connect to127.0.0.1while the origin is attacker-controlled. We checked whether the browser's Same-Origin Policy or CORS would block thePOST— it doesn't: the server sets permissive CORS headers (CORS_ALLOW_ALL), and even without them, a simplePOSTwithContent-Type: text/plainis a CORS-simple request whose side effects fire regardless of whether the response is readable. We checked whethercmd_client.pysets a customHostthat our check might break — it usesurllib, which derivesHostfrom the URL host (127.0.0.1), so it's fine. Finally we considered whether the precondition ("victim visits attacker page while Umi-OCR is running") is realistic — Umi-OCR is a long-running desktop app, so the window is typically the whole session.Discovered by the Sebastion AI GitHub App.