fix(security): close three HIGH-severity API issues#224
Merged
Conversation
* Thumbnail ``color`` / ``size`` / ``radius`` are now whitelisted before
the ``color`` string reaches the cache-filename construction. Values
like ``?color=../../evil`` previously escaped the thumbnail cache
directory via ``Path /`` concatenation and let PIL write a .png to an
arbitrary location.
* ``POST /version/update`` and ``POST /version/restart`` now honour
``PHOTOMAP_INLINE_UPGRADE`` server-side (previously only the UI
button was hidden) and require an ``X-Requested-With: photomap``
header, which forces a CORS preflight that we do not answer — so a
cross-origin simple POST from any page the user visits can no longer
silently trigger a pip install or kill the server. The frontend
already sends the header from ``about.js``.
* InvokeAI settings reject URLs whose scheme is not ``http``/``https``
(and empty-host values like ``http://``) so the config field cannot
be flipped to ``file://`` or ``javascript:`` and used as an SSRF
pivot from ``/status``, ``/boards``, ``/recall`` or
``/use_ref_image``. The ``queue_id`` field is constrained to
``[A-Za-z0-9_.-]{1,64}`` so a request body can't splice ``../`` into
the outbound path and reach arbitrary endpoints on the configured
backend.
New tests: tests/backend/test_thumbnail_validation.py,
tests/backend/test_upgrade_router.py, plus SSRF / queue-id coverage
appended to tests/backend/test_invoke_router.py. 175 backend + 239
frontend tests pass; ruff / eslint / prettier clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``POST /add_album`` accepts arbitrary absolute ``image_paths``, and ``serve_image`` / ``get_image_by_name`` previously returned any file under those paths as long as ``validate_image_access``'s ``is_relative_to`` guard passed — the guard checks *location* only, not *type*. A caller could therefore create an album with ``image_paths=["/etc"]`` and read ``/etc/passwd`` via ``GET /images/<key>/passwd``. Both endpoints now require the resolved file's suffix to be in ``SUPPORTED_EXTENSIONS`` (the same allowlist the indexer uses). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s inline Previously the settings panel silently dropped the 400 returned for invalid URLs (e.g. file:// schemes) and only flipped auth-row visibility for unreachable backends, leaving the user with no feedback. The hint under the URL field now turns red with a warning icon and shows the backend's detail for: invalid scheme/host, unreachable host, and reachable-but-not-InvokeAI servers. Restores the default hint once the URL is valid and reachable. Co-Authored-By: Claude Opus 4.7 (1M context) <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.
Summary
Closes four HIGH-severity findings from the API security scan. All fixes are localized, backwards-compatible for the legitimate UI, and covered by new tests.
H1 — Thumbnail
colorarbitrary-path write (search.py)The
colorquery param was interpolated straight into the on-disk thumbnail cache filename (thumb_dir / f"{...}_{color_hex}_r{radius}.png"). Because Python'sPath /treats/characters in the right-hand string as real path segments,?color=../../evilescaped the cache directory and PIL wrote a PNG to an arbitrary location. Nowcolormust match#?[0-9A-Fa-f]{6}or\d{1,3},\d{1,3},\d{1,3}(what the parser actually supports);sizeandradiusare bounded.H2 — Unauthenticated
POST /version/updateand/version/restart(upgrade.py)Both endpoints run
pip install --upgrade photomapai/ kill the process with no auth. They now:PHOTOMAP_INLINE_UPGRADEserver-side (previously only the UI button was hidden).X-Requested-With: photomap, which forces a CORS preflight we do not answer — so a cross-origin simple POST from any tab the user visits cannot silently trigger either action.about.jsalready sends the header from the legitimate caller.H3 — SSRF via InvokeAI URL +
queue_idpath injection (invoke.py)POST /invokeai/confignow rejects URLs whose scheme is nothttp/https(plus malformed / empty-host values) so the stored URL can't be flipped tofile://,javascript:, or similar and then used as an SSRF pivot from/status,/boards,/recall,/use_ref_image.queue_idonRecallRequest/UseRefImageRequestis now pattern-gated to^[A-Za-z0-9_.-]{1,64}$, so the body can't splice../auth/logininto the outbound URL path and hit arbitrary endpoints on the configured backend.H4 —
add_album→serve_imagearbitrary-file-read chain (search.py)POST /add_albumaccepts arbitrary absoluteimage_paths, andserve_image/get_image_by_namepreviously returned any file under those paths as long asvalidate_image_access'sis_relative_toguard passed — that guard checks location only, not type. A caller could therefore:Both endpoints now require the resolved file's suffix to be in
SUPPORTED_EXTENSIONS(the same allowlist the indexer uses).Originally scored Medium (as filetree enumeration) until I recognised the chain gives an arbitrary-file-read primitive, which is High on its own. New test:
tests/backend/test_image_type_guard.py.Test plan
pytest tests— 177 backend tests pass, including newtest_thumbnail_validation.py,test_upgrade_router.py,test_image_type_guard.py, and SSRF /queue_idcoverage appended totest_invoke_router.pynpm test— 239 frontend tests passmake lint— ruff / eslint / prettier cleanPHOTOMAP_INLINE_UPGRADE=0in the env, Update/Restart buttons return 403 from the backendfetch("http://localhost:8050/version/update", {method:"POST"})from an unrelated page returns 403 / never fires pip?color=#e41a1cetc.)file:///etc/passwdin the InvokeAI URL field surfaces a 400 in the settings panel🤖 Generated with Claude Code