Skip to content

fix(security): sandbox local media paths in cloud AI providers (rebase of #1216) - #1333

Merged
groupthinking merged 5 commits into
mainfrom
land-pr-1216-fixsecurity-sandbox-local-media-paths-grv-296-e2e4
Aug 4, 2026
Merged

fix(security): sandbox local media paths in cloud AI providers (rebase of #1216)#1333
groupthinking merged 5 commits into
mainfrom
land-pr-1216-fixsecurity-sandbox-local-media-paths-grv-296-e2e4

Conversation

@groupthinking

@groupthinking groupthinking commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1330. Supersedes #1216 (its two commits cherry-picked onto current main with conflicts resolved), which was opened for the same work under #1209.

Outcome

Local file reads in the three cloud AI providers' analyze_image paths are now fail-closed: disabled unless CLOUD_AI_MEDIA_ROOT is set, and when enabled every candidate path is resolved (symlinks followed) and must sit inside the resolved root. Escapes and non-regular files raise a typed UnsafeMediaPathError; a misconfigured root raises ConfigurationError. Full rationale, scoping notes, and acceptance-criteria mapping are in #1216.

Scope

Risk

  • Risk level: low
  • Failure mode: callers passing local paths get UnsafeMediaPathError until CLOUD_AI_MEDIA_ROOT is set; no HTTP route reaches analyze_image today.
  • Rollback: revert this PR; no data or schema changes.

Verification

On head of this branch (92b980d):

Production evidence

Not applicable — backend hardening with no user-facing surface; no route currently reaches analyze_image (see #1216).

Agent handoff

groupthinking and others added 3 commits August 4, 2026 03:04
All three cloud AI providers dispatch `analyze_image(image_url, ...)` on the
string's prefix: `s3://` and `http(s)://` are treated as remote sources, and
anything else fell through to an unguarded `open()`. A caller-supplied
absolute path, `../` traversal, or symlink could therefore read any file
readable by the service account.

The same unguarded sink existed in all three providers, not just the one named
in the issue:

  - aws_rekognition.py  `_prepare_image_input`
  - azure_vision.py     `_prepare_image_input`
  - google_cloud.py     inline `open()` in `analyze_image`

Introduce `cloud_ai/media_paths.py` as the single policy for local reads:

  - Local reads are opt-in via `CLOUD_AI_MEDIA_ROOT`. Unset (the default)
    disables them entirely, restricting providers to `s3://`/`https://`.
    This is fail-closed, and answers the issue's open question.
  - When a root is configured, both root and candidate are fully resolved
    (`Path.resolve()` follows symlinks) and the candidate must be contained by
    the root -- covering symlink escapes, not just lexical `..` segments.
  - Non-regular files (FIFO, device, directory) are rejected, so a FIFO placed
    inside the root cannot pin a `to_thread` worker forever.
  - Rejection raises the new typed `UnsafeMediaPathError(CloudAIError)` instead
    of silently returning empty bytes. Each provider re-raises `CloudAIError`
    subclasses unchanged so the type survives to the caller.
  - Providers read from the resolved path, not the caller string, narrowing the
    check-to-open race.
  - Error messages echo only the caller-supplied value; the resolved path is
    logged server-side for forensics rather than returned.

Adds tests/unit/test_cloud_ai_media_paths.py (44 tests) covering absolute
paths, `../` traversal, symlink escape, non-regular files, the disabled
default, and per-provider propagation. Existing local-file tests now set
`CLOUD_AI_MEDIA_ROOT`. Full cloud AI suite: 505 passed.

Closes #1209

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… gaps (#1216)

Addresses the three unresolved Copilot review threads on #1216:

1. Fail-closed on a misconfigured root. get_media_root() left resolve()
   non-strict, so CLOUD_AI_MEDIA_ROOT=/etc/passwd (a regular file) was
   accepted as the root; that file then passed its own is_relative_to()
   containment check and was returned as a permitted read. Require the
   resolved root to be an existing directory, raising ConfigurationError
   otherwise. This also surfaces a nonexistent-directory typo loudly
   instead of silently rejecting every candidate.

2. Cover the Google permitted-file branch. AWS/Azure verified successful
   reads but the Google class only had rejection cases, while the PR's
   coverage table claimed the check for all three providers. Add an
   end-to-end analyze_image test asserting the resolved file's bytes are
   assigned to vision.Image().content.

3. Correct the module docstring. Remote-scheme handling is provider-
   specific: only AWS Rekognition recognises s3:// (Azure and Google
   treat it as a local path, rejected while local reads are disabled),
   and all three accept plain http:// as well as https://.

Focused suite: 47 passed (44 + 3 new). Full cloud AI provider suites:
368 passed. ruff/mypy clean; black formatted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFFcgtEzNyhrxJnHgimcd2
The event-loop offload tests from #1304/#1323 pass raw local paths to
_prepare_image_input/analyze_image; with local reads now fail-closed
behind CLOUD_AI_MEDIA_ROOT, they must opt in via tmp_path, matching the
other pre-existing local-file tests.

Generated with [Linear](https://linear.app/myxstack/issue/GRV-296/land-pr-1216-fixsecurity-sandbox-local-media-paths#agent-session-3138b916)

Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com>
@linear-code

linear-code Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

GRV-296

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

🔍 PR Validation

⚠️ Large PR detected (753 lines changed)

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • [‘architecture-gap’, ‘bug’, ‘ci-cd’, ‘ci/cd’, ‘copilot-rabbit’, ‘documentation’, ‘duplicate’, ‘enhancement’, ‘frontend’, ‘github_actions’, ‘good first issue’, ‘help wanted’, ‘high-priority’, ‘invalid’, ‘javascript’, ‘ml-model’, ‘needs-triage’, ‘pipeline-critical’, ‘placeholder-code’, ‘priority:high’, ‘python’, ‘python:uv’, ‘question’, ‘styling’, ‘tests’, ‘v0’]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1de0d234-1c1e-4f10-a1e1-070bee874ff1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the python label Aug 4, 2026
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Ready Ready Preview, v0 Aug 4, 2026 5:36am

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 5002553.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: NOT_APPLICABLE

Evidence agrees.

Machine-readable verdict
{
  "details": {},
  "reasons": [],
  "verdict": "not_applicable"
}

Workflow evidence

@groupthinking
groupthinking marked this pull request as ready for review August 4, 2026 05:27
Copilot AI balanced review requested due to automatic review settings August 4, 2026 05:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens cloud AI image analysis by sandboxing local media reads.

Changes:

  • Adds a shared, fail-closed path guard and typed errors.
  • Applies validation while preserving worker-thread reads.
  • Adds provider security tests and configuration guidance.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
.env.example Documents the media sandbox setting.
cloud_ai/__init__.py Exports sandbox APIs.
cloud_ai/exceptions.py Adds UnsafeMediaPathError.
cloud_ai/media_paths.py Implements path containment validation.
providers/aws_rekognition.py Guards AWS local image reads.
providers/azure_vision.py Guards Azure local image reads.
providers/google_cloud.py Guards Google local image reads.
test_aws_rekognition_provider.py Configures permitted roots in AWS tests.
test_azure_vision_provider.py Configures permitted roots in Azure tests.
test_google_cloud_provider.py Configures permitted roots in Google tests.
test_cloud_ai_media_paths.py Adds comprehensive sandbox security tests.

Comment thread .env.example Outdated
# To enable local-path reads (dev/self-hosted only), set this to a directory
# that contains ONLY media you are willing to expose. Paths are fully resolved,
# so `../` traversal and symlinks that escape the root are rejected.
# Leave empty in production: use s3:// or https:// inputs instead.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5002553. .env.example now scopes the schemes per provider: http(s):// for all providers, s3:// for AWS Rekognition only, with an explicit note that Azure Vision and Google Vision treat s3:// as a local path (and therefore reject it while local reads are disabled). The production guidance line was updated to match.


Generated by Claude Code

Comment on lines +120 to +122
"Local media reads are disabled. Use an s3:// or https:// source, "
f"or set {MEDIA_ROOT_ENV_VAR} to the directory local media may be "
"read from.",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5002553. The disabled-reads UnsafeMediaPathError message no longer implies s3:// works everywhere — it now reads "https:// works for every provider; s3:// only for AWS Rekognition", so an Azure/Google caller is pointed at a scheme that actually resolves. Guard logic is unchanged; the existing message-substring assertions ("disabled", CLOUD_AI_MEDIA_ROOT) still hold.


Generated by Claude Code

The disabled-reads UnsafeMediaPathError message and .env.example both
suggested s3:// as a recovery scheme for all three cloud AI providers,
but only AWS Rekognition recognizes s3://. Azure Vision and Google
Vision route s3:// through the disabled local-path branch, so following
that guidance just raises UnsafeMediaPathError again.

Reword both to scope s3:// to AWS Rekognition and point Azure/Google
callers at https:// (valid for every provider). No logic change; the
guard behavior is unchanged. Addresses the two Copilot review threads
on this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8yJv2udCCnwN4u586e9PL

Copy link
Copy Markdown
Owner Author

PR-remediation routine — status: HALTED (needs a human decision + one code touch I can't make autonomously)

Ran OBSERVE → conflict → CI gates on head 92b980d. No merge conflict (unstable, not dirty). Blockers, in order:

1. Governance gate red — duplicate canonical issue (your call).
Both PR Governance and Canonical issue and evidence fail with exactly one closing reference is required. Two causes:

Decision needed: close the superseded #1216, then set this body to Closes #1209. I did not close #1216 or edit either body on my own — choosing which of two duplicate PRs to retire is a human call, and the "supersedes" note was routine-authored, not human-confirmed.

2. Two unresolved Copilot review threads — need a commit.
Both flag the same thing: s3:// is presented as universal, but only AWS Rekognition recognizes it — Azure Vision and Google Vision treat s3:// as a local path and raise UnsafeMediaPathError.

  • .env.example:168 — document provider-specific schemes instead of implying s3:// works everywhere.
  • src/youtube_extension/integrations/cloud_ai/media_paths.py:122 — make the UnsafeMediaPathError message provider-neutral (or derive it from the provider's supported schemes) so the suggested recovery path is valid for every caller.

I can't push this fix — my branch guardrail restricts me to my own dev branch. Staged for whoever picks it up.

3. Merge is human-gated. main is protected and this PR is not labeled automerge, so per policy I do not auto-merge. Once 1–2 are cleared and CI is green, the staged action is a squash-merge of #1333 into main.

CI note: at scan time test, Generate and Upload Coverage, and trivy were still in flight; the red Vercel status is a dashboard cancel, not a real failure.


Generated by Claude Code

@groupthinking
groupthinking merged commit 1f0ca9c into main Aug 4, 2026
40 of 41 checks passed
@groupthinking
groupthinking deleted the land-pr-1216-fixsecurity-sandbox-local-media-paths-grv-296-e2e4 branch August 4, 2026 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Land PR #1216: fix(security): sandbox local media paths

3 participants