Skip to content

fix(client): skip symlinks in directory uploads#2374

Merged
t0saki merged 1 commit into
volcengine:mainfrom
ehz0ah:fix/http-directory-upload-symlink-hardening
Jun 2, 2026
Merged

fix(client): skip symlinks in directory uploads#2374
t0saki merged 1 commit into
volcengine:mainfrom
ehz0ah:fix/http-directory-upload-symlink-hardening

Conversation

@ehz0ah
Copy link
Copy Markdown
Contributor

@ehz0ah ehz0ah commented Jun 1, 2026

Summary

  • Skip file and directory symlink entries when the HTTP client zips a local directory for upload.
  • Keep uploaded directory archives bounded to real files under the selected root before sending them to /api/v1/resources/temp_upload.
  • Warn when directory packaging produces an empty archive, such as a directory containing only skipped symlinks.
  • Add regression coverage for file symlinks, out-of-root symlinks, directory symlinks, and empty archives.

Why

Directory uploads are packaged on the client before they reach the server. Once a symlink target is followed into the zip, the server only sees a regular archive entry and cannot tell whether it came from outside the chosen directory. This keeps the local upload boundary enforced at packaging time.

Symlink entries are skipped as aliases, whether they point at files or directories. Real files under the selected root are still included through their normal paths, so in-root symlink targets are not lost. For example, if upload/alias points at upload/data, the archive includes data/... when that real path is walked, but not duplicate alias/... entries.

This PR intentionally does not change the existing behavior for files that vanish or become unreadable while the archive is being written. That zipf.write(...) race predates this patch and has different upload-completeness semantics from symlink hardening.

Validation

  • uv run --frozen --extra dev ruff check openviking_cli/client/http.py tests/client/test_rebuild_clients.py
  • uv run --frozen --extra dev ruff format --check openviking_cli/client/http.py tests/client/test_rebuild_clients.py
  • uv run --frozen --extra test pytest tests/client/test_rebuild_clients.py --no-cov -q
  • Local server smoke with temporary HOME, config, and storage under /tmp/ov-symlink-smoke.*: uploaded a directory containing regular files plus file, external, and directory symlinks; both the client-created zip and server-stored temp upload contained only keep.txt and nested/nested.txt.

Additional broader check:

  • OPENVIKING_CLI_CONFIG_FILE=/tmp/openviking-nonexistent-ovcli.conf uv run --frozen --extra test pytest tests/client --no-cov -q currently reports 109 passed and 2 failures in existing relation/watch tests outside this upload path.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

PR Reviewer Guide 🔍

(Review updated until commit 19b545f)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🏅 Score: 95
🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Unhandled Exceptions in Path Resolution

The defense-in-depth check calls file_path.resolve() without handling exceptions like FileNotFoundError or PermissionError. These could propagate and fail the entire zip operation if a file vanishes after the is_file() check or is unreadable. Consider catching more exceptions here to skip problematic files or log them appropriately.

try:
    file_path.resolve().relative_to(root)
except ValueError:
    continue

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Use is_relative_to() for path check

Replace the try-except block using relative_to() with pathlib's is_relative_to()
method. This avoids using exceptions for control flow and makes the path validation
logic more readable and idiomatic.

openviking_cli/client/http.py [356-359]

-try:
-    file_path.resolve().relative_to(root)
-except ValueError:
+resolved_path = file_path.resolve()
+if not resolved_path.is_relative_to(root):
     continue
Suggestion importance[1-10]: 6

__

Why: Replaces exception-based control flow with idiomatic pathlib is_relative_to() for better readability and maintainability, accurately targeting the existing code block.

Low

@ehz0ah ehz0ah force-pushed the fix/http-directory-upload-symlink-hardening branch 2 times, most recently from fab01b3 to 19b545f Compare June 2, 2026 05:57
@ehz0ah ehz0ah marked this pull request as ready for review June 2, 2026 05:58
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

Persistent review updated to latest commit 19b545f

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

PR Code Suggestions ✨

No code suggestions found for the PR.

@ehz0ah ehz0ah force-pushed the fix/http-directory-upload-symlink-hardening branch from 19b545f to ecc4a6f Compare June 2, 2026 07:08
@ehz0ah ehz0ah changed the title fix(client): skip symlinks in directory uploads fix(client): skip file and directory symlinks in uploads Jun 2, 2026
@ehz0ah ehz0ah changed the title fix(client): skip file and directory symlinks in uploads fix(client): skip symlinks in directory uploads Jun 2, 2026
Copy link
Copy Markdown
Collaborator

@t0saki t0saki left a comment

Choose a reason for hiding this comment

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

LGTM

@t0saki t0saki merged commit e0cf9e0 into volcengine:main Jun 2, 2026
5 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in OpenViking project Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants