Skip to main content

review-topic-input-boundary-security

Load when a PR touches SSRF/URL-fetch guards (`_ensure_url_input_allowed`, `_ensure_resource_fqdn_allowed`, `WHITELISTED_DESTINATIONS_FOR_URL_INPUT`/`BLACKLISTED_DESTINATIONS_FOR_URL_INPUT`), local-file image loads (`ALLOW_LOADING_IMAGES_FROM_LOCAL_FILESYSTEM`), model-cache paths from a `model_id` (`cache_path_is_within_root`, `MODEL_CACHE_DIR`), unsafe deserialization (`pickle.loads`, `torch.load`, `weights_only`, `allow_untrusted_packages`), or archive extraction (`tarfile.extractall`, `ZipFile`).

Zur Installation springen

Quellinformationen

Repository
roboflow/inference
Letzte Quellaktivität
2. Juli 2026 um 12:51
Erkannte Sprache von SKILL.md
Englisch
Sterne
2.448
Forks
320

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
review-topic-input-boundary-security
description
Load when a PR touches SSRF/URL-fetch guards (`_ensure_url_input_allowed`, `_ensure_resource_fqdn_allowed`, `WHITELISTED_DESTINATIONS_FOR_URL_INPUT`/`BLACKLISTED_DESTINATIONS_FOR_URL_INPUT`), local-file image loads (`ALLOW_LOADING_IMAGES_FROM_LOCAL_FILESYSTEM`), model-cache paths from a `model_id` (`cache_path_is_within_root`, `MODEL_CACHE_DIR`), unsafe deserialization (`pickle.loads`, `torch.load`, `weights_only`, `allow_untrusted_packages`), or archive extraction (`tarfile.extractall`, `ZipFile`).
# Review topic: Input-boundary security (SSRF, path traversal, unsafe deserialization, decompression) ## When this applies Trigger on CONTENT/behaviour, not just directory. Load when the diff: - Fetches a resource from a user/client-supplied URL, or edits any of the URL guards in `inference/core/utils/image_utils.py` — `load_image_from_url`, `_ensure_url_input_allowed`, `_ensure_resource_schema_allowed`, `_ensure_resource_fqdn_allowed`, `_concatenate_chunks_of_network_location`, `_ensure_location_matches_destination_whitelist`/`_blacklist`. - Changes URL-input policy env in `inference/core/env.py` — `ALLOW_URL_INPUT`, `ALLOW_NON_HTTPS_URL_INPUT`, `ALLOW_URL_INPUT_WITHOUT_FQDN`, `WHITELISTED_DESTINATIONS_FOR_URL_INPUT`, `BLACKLISTED_DESTINATIONS_FOR_URL_INPUT`, `ALLOW_LOADING_IMAGES_FROM_LOCAL_FILESYSTEM`, `ALLOW_NUMPY_INPUT`. - Builds a filesystem path from a `model_id`, filename, or other client string — `get_cache_dir`, `get_model_id_cache_path`, `slugify_model_id_to_cache_key`, `cache_path_is_within_root`, `get_cache_file_path`, or any `os.path.join(MODEL_CACHE_DIR, <user string>)`. - Deserializes untrusted bytes — `pickle.loads`, `torch.load`, `np.load`, `joblib.load`, or toggles `weights_only` / `allow_untrusted_packages` / `ALLOW_INFERENCE_MODELS_UNTRUSTED_PACKAGES`. - Extracts an archive — `tarfile.extractall`, `zipfile.ZipFile`, `shutil.unpack_archive` — or decodes an image/embedding whose size is attacker-controlled. ## Review checklist Severity-tag every finding. Reference the numbered Standards below. - **BLOCK** — A user-supplied URL reaches `requests.get` (or any egress) without passing through the full `load_image_from_url` guard chain (`_ensure_url_input_allowed` → schema → FQDN → whitelist → blacklist). [S1] - **BLOCK** — The URL that is validated is not the URL that is fetched: validation runs on the raw string while the request follows redirects, or `requests.get(...)` is given a different URL than the one whose host was checked. The prepared/normalized URL must be both validated and fetched, and redirects to unvalidated hosts must not be silently followed (GHSA-hjmm-hr52-vrp2 / PR #2501). [S1][S2] - **BLOCK** — Host parsing that a `\`, `@`, userinfo, or bracketed-IPv6 authority can bypass so the allow/deny check sees a different host than `requests` connects to (GHSA-rv25-cq3f-x5f2 / PR #2500). [S2] - **BLOCK** — `pickle.loads` / `torch.load(weights_only=False)` / `np.load(allow_pickle=True)` newly applied to bytes an unauthenticated client can supply, without an explicit trust gate. [S5] - **BLOCK** — `tarfile.extractall` / `ZipFile.extractall` on an archive whose member paths are not confirmed to stay within the destination (Zip-Slip / `../` traversal), or with no total-size / member-count bound (decompression bomb). [S6] - **BLOCK** — A `model_id` or filename is joined into a path and used to read/write without the `cache_path_is_within_root` containment check, letting `..`/absolute segments escape `MODEL_CACHE_DIR`. [S4] - **FLAG** — A new client-string-derived path skips `slugify_model_id_to_cache_key` and relies on the raw value staying benign. [S4] - **FLAG** — Weakening a URL guard default: flipping `ALLOW_NON_HTTPS_URL_INPUT` / `ALLOW_URL_INPUT_WITHOUT_FQDN` / `ALLOW_LOADING_IMAGES_FROM_LOCAL_FILESYSTEM` to a more-permissive default, or bypassing the whitelist/blacklist check for a "trusted" branch. [S3][S7] - **FLAG** — `os.path.isfile(user_value)` / `cv2.imread(user_value)` reachable when `ALLOW_LOADING_IMAGES_FROM_LOCAL_FILESYSTEM` is disabled — local-FS reads must be gated by that flag on every path (PR #957). [S7] - **FLAG** — `torch.load` left at `weights_only=False` for a NEW weights path that could load `safetensors`/`weights_only=True` instead, or a new `allow_untrusted_packages=True` call site. [S5] - **FLAG** — Image/embedding decode with no upper bound on decoded dimensions/bytes where the input is client-supplied (decompression bomb via `cv2.imdecode` / `np.load`). [S6] - **NIT** — A guard whose failure message leaks the internal target it tried to reach, or a swallowed decode exception that hides which guard rejected the input. ### Not blocking - Do NOT flag `torch.load(weights_only=False)` on the OWLv2 embeddings cache or transformers/qwen tar extraction when the bytes originate from `MODEL_CACHE_DIR` populated by the Roboflow weights API, not a request body — that is a first-party artifact, not attacker input. Call it out only if a new codepath lets a client supply those bytes. - Do NOT demand a whitelist/blacklist when `WHITELISTED_DESTINATIONS_FOR_URL_INPUT` / `BLACKLISTED_DESTINATIONS_FOR_URL_INPUT` are unset — they are opt-in operator controls (`None` ⇒ skip), and the schema/FQDN guards still apply. - Do NOT treat `pickle.loads` in `load_image_from_numpy_str` as a new hole — it is gated behind `ALLOW_NUMPY_INPUT` (default `False`) and is pre-existing; flag only if the gate is removed or the default flips. - Auth / api-key / tenant-boundary concerns are owned by `review-topic-auth-and-tenant-security` — cross-ref, do not re-review here. `review-core-infra` keeps the `image_utils` SSRF chain as its surface instance and cross-refs this topic for the deep rules. ## What to check / Standards **S1 — All user URLs run the full guard chain.** In `load_image_from_url`, a client URL must pass `_ensure_url_input_allowed()` (respects `ALLOW_URL_INPUT`), then `_ensure_resource_schema_allowed`, `_ensure_resource_fqdn_allowed`, `_ensure_location_matches_destination_whitelist`, and `_ensure_location_matches_destination_blacklist` BEFORE any egress. Any new URL-fetching entry point must route through the same guards, not re-implement a subset. The default posture is https-only (`ALLOW_NON_HTTPS_URL_INPUT=False`) and FQDN-required (`ALLOW_URL_INPUT_WITHOUT_FQDN=False`) (PR #497 introduced the guard chain and these env controls). **S2 — Validate-then-fetch the same, normalized URL; parse the real authority.** The host that the guards check must be the host `requests` connects to. `load_image_from_url` rejects a `\` in the raw `netloc`, `prepare()`s the request, then validates and fetches `prepared_url` — do not validate the raw string and fetch something else, and do not let the parsed host diverge from the connected host via userinfo (`@`), backslash, or IPv6-bracket tricks (GHSA-rv25-cq3f-x5f2 / PR #2500). Host extraction uses `parsed_url.hostname` (bracketing IPv6) fed to `tldextract`, not raw `netloc`. Redirects must not smuggle the request to an internal target after a benign first host passes validation (GHSA-hjmm-hr52-vrp2 / PR #2501). **S3 — Whitelist/blacklist are exact-destination allow/deny lists.** `_ensure_location_matches_destination_whitelist` rejects unless the concatenated network location is in `WHITELISTED_DESTINATIONS_FOR_URL_INPUT`; `_ensure_location_matches_destination_blacklist` rejects if it is in `BLACKLISTED_DESTINATIONS_FOR_URL_INPUT`. Both are `None`-means-skip. The destination is `_concatenate_chunks_of_network_location(...)` over the tldextract result (subdomain.domain.suffix, IPv6 brackets stripped) — do not compare against raw user text or a substring, which would let `evil.com/../roboflow.com` or a subdomain trick slip through. **S4 — model_id / filename paths must stay inside MODEL_CACHE_DIR.** `get_model_id_cache_path` only trusts the raw `model_id` as a path segment when `cache_path_is_within_root` (an `os.path.commonpath` containment check against the abspath of `cache_dir_root`) AND `path_fits_os_limits` both pass; otherwise it falls back to `slugify_model_id_to_cache_key` (regex to `[A-Za-z0-9_-]`, collapse, truncate, blake2s suffix). Any new path built from a client string must apply the same containment check or slugify — never `os.path.join(MODEL_CACHE_DIR, raw_model_id)` and read/write directly, which a `../` or absolute `model_id` would escape. **S5 — Deserialize untrusted bytes safely.** Never `pickle.loads`, `torch.load(weights_only=False)`, or `np.load(allow_pickle=True)` on bytes a client can supply without an explicit trust gate. Prefer `weights_only=True` (as `doctr_model.py` and `perception_encoder/pe.py` do) and `safetensors`. `allow_untrusted_packages` must stay bound to `ALLOW_INFERENCE_MODELS_UNTRUSTED_PACKAGES` (env, default `False`) — a literal `True` at a call site is a finding. The one client-facing pickle (`load_image_from_numpy_str`) stays behind `ALLOW_NUMPY_INPUT` (default `False`); keep it that way. **S6 — Bound decompression and extraction.** Archive extraction (`tarfile.extractall`, `ZipFile`) must confirm every member resolves within the destination (no `../`, no absolute, no symlink escape) and bound total uncompressed size / member count. Image and embedding decodes (`cv2.imdecode`, `np.load(BytesIO(...))`) on client bytes must have an upper bound on decoded dimensions/bytes so a small payload cannot expand into an OOM. Only exempt archives sourced from the first-party weights cache, not request bodies. **S7 — Do not weaken input-boundary defaults.** `ALLOW_URL_INPUT=True` but `ALLOW_NON_HTTPS_URL_INPUT=False`, `ALLOW_URL_INPUT_WITHOUT_FQDN=False`, `ALLOW_NUMPY_INPUT=False` are the safe defaults; `ALLOW_LOADING_IMAGES_FROM_LOCAL_FILESYSTEM` gates every local-FS read (both `load_image_with_known_type` for `ImageType.FILE` and the inferred `os.path.isfile` branch — PR #957). Flipping any of these to more-permissive defaults, or adding a "trusted"/internal bypass around a guard, needs explicit justification and is at least a FLAG. ## Key files & reference PRs - `inference/core/utils/image_utils.py` — SSRF guard chain: `load_image_from_url` (validate-then-fetch on the prepared URL), `_ensure_url_input_allowed`, `_ensure_resource_schema_allowed`, `_ensure_resource_fqdn_allowed`, `_concatenate_chunks_of_network_location`, `_ensure_location_matches_destination_whitelist`/`_blacklist`; local-FS gating in `load_image_with_known_type` / `load_image_with_inferred_type`; the `ALLOW_NUMPY_INPUT`-gated `pickle.loads` in `load_image_from_numpy_str`. - `inference/core/env.py` — URL/input policy: `ALLOW_URL_INPUT`, `ALLOW_NON_HTTPS_URL_INPUT`, `ALLOW_URL_INPUT_WITHOUT_FQDN`, `WHITELISTED_DESTINATIONS_FOR_URL_INPUT`, `BLACKLISTED_DESTINATIONS_FOR_URL_INPUT`, `ALLOW_LOADING_IMAGES_FROM_LOCAL_FILESYSTEM`, `ALLOW_NUMPY_INPUT`, `ALLOW_INFERENCE_MODELS_UNTRUSTED_PACKAGES`, `MODEL_CACHE_DIR`. - `inference/core/cache/model_artifacts.py` — path-traversal containment: `get_cache_dir`, `get_model_id_cache_path`, `cache_path_is_within_root`, `slugify_model_id_to_cache_key`, `path_fits_os_limits`, `get_cache_file_path`. - `inference/core/models/inference_models_adapters.py` — `allow_untrusted_packages=ALLOW_INFERENCE_MODELS_UNTRUSTED_PACKAGES` call sites (the pattern every model adapter must follow). - `inference/models/**` — `torch.load` sites: `weights_only=True` in `doctr/doctr_model.py`, `perception_encoder/vision_encoder/pe.py`; `weights_only=False` on the first-party OWLv2 embeddings cache in `owlv2/owlv2.py`; `tarfile.extractall` in `transformers/transformers.py`, `qwen25vl/qwen25vl.py`, `qwen3vl/qwen3vl.py`; `np.load(BytesIO(...))` embedding/mask decode in `sam/`. - Reference PRs / advisories: #2500 (GHSA-rv25-cq3f-x5f2 — backslash/userinfo authority-parsing allowlist bypass), #2501 (GHSA-hjmm-hr52-vrp2 — validate-first-FQDN-then-follow-redirect-to-internal-target), #497 (original SSRF guard chain + URL-input env controls), #957 (gate local-filesystem image reads behind `ALLOW_LOADING_IMAGES_FROM_LOCAL_FILESYSTEM`, stop passing user value to `os.path.isfile`).
Auf GitHub ansehen