Lock down the local FastAPI server against unauthenticated access - #266
Open
JCTec wants to merge 1 commit into
Open
Lock down the local FastAPI server against unauthenticated access#266JCTec wants to merge 1 commit into
JCTec wants to merge 1 commit into
Conversation
The desktop backend bound 127.0.0.1:8765 with no authentication and CORS *. Any local process or web page that can reach loopback could start GPU jobs, change the Hugging Face token, or read files through /workspace path traversal and /optimize/serve-file. Require a per-session bearer token when Electron starts the API. Reject non-loopback Host and Origin unless MODLY_API_ALLOW_REMOTE=1. Jail workspace, export, optimize, and serve-file paths. Copy imported meshes into a Modly temp dir so serve-file never reads arbitrary disks. Inject the token from Electron (main-process axios and renderer webRequest) so the 3D viewer and download links keep working. Teach the CLI and MCP client to send --token, MODLY_API_TOKEN, or the userData/api-token file. Stop putting the Hugging Face token in query strings. GET /health stays public so readiness probes still work. Headless uvicorn without MODLY_API_TOKEN remains usable on a trusted machine.
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.
Lock down the local FastAPI server
Summary
The desktop backend listens on
127.0.0.1:8765with no authentication andCORS *. Any local process, and any web page that can reach loopback, can:/workspace/{path}(no resolve + jail).glb/.splatfiles via/optimize/serve-file?path=This PR closes that local attack surface without breaking the Electron UI, the 3D viewers, or the CLI/MCP automation contract.
What changed
Access control (
api/services/api_guard.py)Three layers, applied to every route except
GET /healthand CORSOPTIONS:Loopback
Host— reject DNS-rebinding style hosts (evil.example).Local
Origin— reject browser requests from non-local pages. CLI/curl send noOriginand still work.Bearer token when Electron starts the API — Electron mints a 32-byte hex token, sets
MODLY_API_TOKEN, and writesuserData/api-token(0600). Requests need:or
X-Modly-Token: <token>(constant-time compare).GET /healthstays public so readiness probes still work. It now reports{ "status": "ok", "auth": "required" | "off" }.Headless
uvicornwithoutMODLY_API_TOKENremains usable on a trusted machine (token optional). Host/Origin checks stay on. Binding beyond loopback (Jetson LAN,--host 0.0.0.0) requires:CORS is no longer
*. It allowsnull,file://,app://, andhttp(s)://localhost|127.0.0.1|[::1]so the packaged app andnpm run devstill work.Content-Lengthstays exposed for drei / splat loaders.Path jail (
api/services/local_paths.py)/workspace/…,/export/…,/optimize/export,/optimize/ply-to-splatonly serve workspace-relative paths..., absolute paths, and Windows drive prefixes are rejected./optimize/mesh,/smooth,/transformaccept workspace-relative paths, or an already-imported file under a Modly temp dir./optimize/serve-fileno longer reads arbitrary disk paths. Allowed roots: the workspace, or process temp files namedmodly_splat_*/ living undermodly_import_*.POST /extensions/setup/{ext_id}rejects path-like extension ids..glb/.splatinto amodly_import_*temp dir before serving, so the original user file is not re-exposed over HTTP.Electron
apiHttpclient that attaches the token. GitHub tarball downloads are unchanged (still rawaxios).useGLTF, splat viewer,<a download>,fetchin ChatPanel) cannot set a secret header.session.webRequest.onBeforeSendHeadersinjects the token forhttp://127.0.0.1:8765/*andhttp://localhost:8765/*only.X-HuggingFace-Tokeninstead of?token=on the query string.CLI and MCP
--token,MODLY_API_TOKEN, thenuserData/api-token(macOS / Windows / Linux Electron paths).dev serve-apialways generates a token for the child process and prints it in the JSON (it is not written over Electron’s file).--allow-remote(or a non-loopback--host) setsMODLY_API_ALLOW_REMOTE=1.Docs and tests
api/README.md,tools/modly-cli/SKILL.md,docs/running-on-jetson.mddocument the token and the remote flag.api/tests/test_api_guard.py,api/tests/test_local_paths.py, CLI auth cases.scripts/run-pytests.mjsalso runstools/modly-cli/test_agent.py.What this does not change
Out of scope on purpose (happy to follow up):
setup.py/npm installfor anygithub.comURL. The “trusted” badge is still cosmetic.settings.json.fs:readFileBase64,shell:openExternal,fs:deleteDirectoryprefix check) is unchanged.How this was tested
Unit tests only on the author’s machine. The desktop app was not launched, and the live API was not hit end-to-end.
api/tests/test_local_paths.pyapi/tests/test_api_guard.py(FastAPI + httpx venv)X-Modly-Token200,ALLOW_REMOTEstill requires tokentest_runner/test_extension_processtools/modly-cli/test_agent.pyauth casespy_compileon changed Python filesnpm test/tscnode_modulesin that checkout)npm run dev/ packaged appcurlagainst:8765webRequestheader injecttest_api_guard.pyskips if FastAPI is not installed in the test interpreter. One pre-existing CLI failure remains on macOS:test_batch_accepts_manifest_json(/varvs/private/var). Unrelated to this change.Suggested tests before merge
npm test ./node_modules/.bin/tsc --noEmit -p tsconfig.node.jsonWith the app running (
npm run devand, if possible, a packaged Apple Silicon build):UI checks that exercise the token inject + path jail:
/workspace/…)..glband a.splatfrom outside the workspace; optimize / smooth / export still work.GET /workspace/../…andGET /optimize/serve-file?path=/etc/passwd(or a home-dir.glb) return 400, not the file.file://window: viewer, download link, and ChatPanelfetchstill authorize.Headless / Jetson:
Questions for maintainers
These are product calls this patch had to guess. Happy to change the PR to match.
Blocking for this change
Is headless
uvicornwithout a token still a supported mode?Electron always sets
MODLY_API_TOKEN. Bareuvicorn main:appstays open except for Host/Origin checks. Fail-open for dev, or fail-closed everywhere?Is
MODLY_API_ALLOW_REMOTE=1+ token the right LAN story?The Jetson guide tells people to hit the API from another machine. If LAN access is first-class, it may belong in settings rather than an env escape hatch.
Is the token file the official CLI contract?
Electron writes
userData/api-tokenand rotates it every launch. CLI order is--token→MODLY_API_TOKEN→ that file. Fine, or Unix socket / env-only / Electron-only?Must
GET /healthstay unauthenticated?Needed for readiness. It also advertises that Modly is running (
auth: required|off).Did tightening CORS break the splat/GLB viewers?
Especially packaged Apple Silicon (
file:///Origin: null) vsnpm run dev(localhost:5173). This was not runtime-tested here.Is copying imports into
modly_import_*temp OK?Large files are duplicated; temp dirs can vanish. Prefer
workspace/Imports/instead?Follow-ups (not in this PR)
Should “trusted extension” actually block install?
Any
github.com/owner/repostill runssetup.py/npm installas the user. The registry badge is cosmetic. Confirm dialog, official-only default, or signed manifests?api/resources/public_key.pemlooks like unused leftover signing.Any plan to Developer ID–sign and notarize the macOS DMG?
Auto-update is already off on Darwin because the build is ad-hoc only.
Should the HF token leave plaintext
settings.json?Query-string leak is fixed; disk + process env remain.
IPC follow-up?
fs:readFileBase64(any path),shell:openExternal(no scheme allow-list),fs:deleteDirectory(startsWith). Same class of bug, different process.Pin a checksum for the python-embed tarball?
scripts/download-python-embed.jscurrently trusts HTTPS GitHub with no hash.If only two of these need an answer before merge: (1) fail-open vs fail-closed and (5) did the viewer break.