Repository navigation
Plug security holes and redeploy the public demo - #113
Merged
Merged
Conversation
- Add SYNANNO_PUBLIC_DEMO (off by default). In demo mode, /load_materialization accepts only the bundled H01 table, and /upload and /launch_neuroglancer reject credential uploads and any volumes other than the public H01 release with 403. - Outside demo mode, /load_materialization accepts only paths under SYNANNO_DATA_DIRS (default: h01/) and http(s) URLs. Validation runs before the cached-table check, and errors no longer echo exception text. - Remove CORS: no front-end code makes cross-origin requests to Flask. - run.py binds to 127.0.0.1 and enables the debugger only with FLASK_DEBUG=1; DEBUG_APP defaults to off. - Read SECRET_KEY from the environment, or generate one with a warning. - Stop exposing the os module to Jinja templates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The token was randint(0, 3200), so anyone could enumerate /v/<token>/ on the public port 9015. Use secrets.token_urlsafe(32) instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jQuery 2.1.3 has known XSS issues (CVE-2015-9251, CVE-2019-11358, CVE-2020-11022, CVE-2020-11023). Serve 3.7.1 from static/vendor (sha256 matches the official SRI hash) and replace the .load(handler) shorthand removed in jQuery 3 with .on("load", handler). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file routes serve images only from in-memory dicts keyed by the URL segments, so they never touch the filesystem. Lock that in with tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
calculate_crop_pad used assert to reject invalid bounding boxes, which python -O strips. Raise ValueError instead, and have /ng_bbox_fn_save return 400 for missing or invalid coordinates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
requirements.lock is pip freeze from the running image (synanno/uwsgi:rollback-2026-10), minus the app's own editable install and flask-cors. The Dockerfile pins the base image to the same Python (3.11.13, bookworm) and pip, and installs the lock with --no-deps, taking the CPU-only torch build from the PyTorch index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Decode canvas uploads as PNG only. /save_canvas passed user-supplied base64 to Image.open, which exposed every Pillow decoder (PSD, FITS, PDF, JPEG2000, font files) and their open advisories. Non-PNG input now returns 400. - Raise tornado from 6.5.1 to 6.5.10. It serves Neuroglancer on the public port 9015, and 6.5.1 has several HTTP-parsing DoS advisories. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first line of the pip freeze output was cut off when the lock was written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/smoke_test.sh <URL> checks that the demo pages load, that the Neuroglancer view is served from <host>:9015 with a long token, that /load_materialization rejects local files and external URLs, that no CORS header is sent, and that credential uploads are refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge variants into single or parametrized tests and drop checks that guarded no change (the tornado pin, the vendored file's header, extra file-route paths). 13 tests; all 11 fix tests fail against main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repository is public. Replace the AWS account ID, instance, Elastic IP allocation, security group, Auto Scaling group and key pair names with placeholders, and recommend digest-pinned rollback revisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--extra-index-url let pip take any locked package from the PyTorch index (it served Jinja2 and MarkupSafe). Install torch/torchvision from that index and everything else from PyPI only. The resulting packages are identical to the deployed image (only the from-source neuroglancer extension differs byte-wise). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- decode_image refuses PNGs larger than 4096 px per side, checked from the header before any pixels are decoded. - /ng_bbox_fn_save restores the shared center point when it rejects a bounding box, instead of leaving the invalid values in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Plug security holes and redeploy the public demo
The public demo (README links) was unreachable: its links pointed at an IP that AWS released when the Auto Scaling group replaced the instance in May 2026. The demo now has an Elastic IP and runs this branch.
Security fixes
A new env flag
SYNANNO_PUBLIC_DEMO(off by default, on in the deployed task) locks the app down to the bundled H01 data./load_materializationpd.read_csv(file read, SSRF); errors echoed exception textSYNANNO_DATA_DIRS(defaulth01/) and http(s) URLs. 400 otherwise, checked before the cached-table shortcut; no exception text in responsesrandint(0, 3200)secrets.token_urlsafe(32)CORS(app)+@cross_originon every routeflask-corsdroppedrun.pyrandebug=Trueon 0.0.0.0;DEBUG_APPdefaulted to Truerun.pybinds 127.0.0.1, debug only withFLASK_DEBUG=1;DEBUG_APPdefaults off; uWSGI never debugsSECRET_KEYfrom env, else random per start with a warningosexposed to Jinja.load(fn)→.on("load", fn)calculate_crop_padvalidated a user bbox withassert; a rejected box still changed shared stateValueError→ 400 from/ng_bbox_fn_save, shared state restoredBuild
requirements.lock=pip freezeof the image that ran before, minus the app's editable install and flask-cors, with tornado bumped.Dockerfile_uwsgi: base pinned topython:3.11.13-bookworm, pip 25.2, installs the lock with--no-deps: torch/torchvision only from the PyTorch CPU index, everything else only from PyPI. apt packages and rustup stay unpinned.pip freezematches the lock exactly.Tests
tests/test_security.py: 13 tests, one per fix (variants parametrized), plus the 3 existing ones; all 16 pass. Againstmain, all 11 fix tests fail; the 2 file-route tests pass there too (those routes were already safe).scripts/smoke_test.sh <demo URL>against the live demo: all 11 checks pass (pages load; Neuroglancer served from<host>:9015with a 43-char token;/etc/passwd,file:///etc/passwdand an external URL rejected with 400; no CORS header; credential upload 403). Against the previous image it fails the 6 security checks.Deployment changes
PUBLIC_DNS_SYNANNO(the Elastic IP) andSYNANNO_PUBLIC_DEMO=1. A separate rollback revision pins the previous image by digest.docs/deploy.mddescribes releases, rollback, re-associating the Elastic IP after an instance replacement, and temporary SSH access. It uses placeholders; concrete account and resource IDs are kept out of the repo.Known issues not fixed here
/resetand/demowipe it (by design: single-user test deployment)./update_image_tiles/1sometimes never reaches the Flask worker (seen on old and new images alike; not with curl or in the browser test). Not yet diagnosed.pip-auditadvisories in packages no request reaches (outbound clients to Google Cloud, decompression, the bundled torch checkpoint): urllib3, requests, httplib2, brotli, pyasn1, protobuf, fsspec, orjson, torch, plus moderate ones in Flask/Werkzeug/Jinja2. bandit has no high findings; the mediums are intended 0.0.0.0 binds in the container, the fixed/tmp/flask_sessiondir, andtorch.loadof the bundled checkpoint./load_materializationreturns 500 once a table is cached (pre-existing DataFrame truthiness bug); bad paths are rejected before that.