Skip to content

fix(artifacts): harden artifact ID validation and absolute_path#2860

Merged
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/artifacts-security-hardening
May 28, 2026
Merged

fix(artifacts): harden artifact ID validation and absolute_path#2860
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/artifacts-security-hardening

Conversation

@M3gA-Mind
Copy link
Copy Markdown
Contributor

@M3gA-Mind M3gA-Mind commented May 28, 2026

Summary

Security hardening for the artifacts persistence layer introduced in #2801:

  • Reject . as artifact IDvalidate_artifact_id in store.rs now explicitly rejects ".", which previously resolved to the artifacts root directory itself, allowing operations to target the root instead of a specific artifact subdirectory
  • Harden absolute_path in ai_get_artifactops.rs now verifies that artifacts_root.join(&meta.path) still starts with artifacts_root before returning the path, guarding against corrupt or adversarial meta.path values in stored meta.json files that could expose paths outside the artifacts sandbox
  • Fix get_artifact schema output shape — The schema in schemas.rs previously declared a single "artifact": ArtifactMeta wrapper output, but the actual JSON response returns all ArtifactMeta fields flat at the top level plus an absolute_path field. Schema now matches the real response shape.
  • Add validate_artifact_id_rejects_dot test in store_tests.rs

Test plan

  • GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml passes (only pre-existing warnings)
  • cargo fmt --manifest-path Cargo.toml --check passes
  • New test validate_artifact_id_rejects_dot covers the . rejection
  • Schema test schemas_get_artifact_requires_artifact_id now asserts flat output fields including absolute_path and absence of the "artifact" wrapper

Notes

This PR was not included in the original #2801 because it was merged before these fixes could be applied. These are follow-up hardening changes with no functional regression risk.

  • N/A: no UI changes
  • N/A: no breaking RPC contract changes (schema fix makes it more accurate, not less)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced artifact path validation to prevent directory traversal vulnerabilities and unauthorized access to files outside the designated artifact directory
    • Improved artifact ID validation by rejecting reserved path components that could create security issues
  • Improvements

    • Simplified artifact API response format to return artifact details directly, improving usability for API consumers

Review Change Stack

- Reject "." as a valid artifact ID in validate_artifact_id (store.rs);
  previously this resolved to the artifacts root directory itself
- Verify that meta.path stays within the artifacts root before returning
  absolute_path in ai_get_artifact (ops.rs), guarding against corrupt or
  adversarial path values stored in meta.json
- Fix get_artifact schema output to reflect the actual flat JSON response
  shape (id, kind, title, path, size_bytes, status, created_at,
  absolute_path) rather than a misleading "artifact" wrapper ref
- Add validate_artifact_id_rejects_dot test in store_tests.rs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@M3gA-Mind M3gA-Mind requested a review from a team May 28, 2026 17:28
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

📝 Walkthrough

Walkthrough

The PR adds path-safety validation to the artifact operations and restructures the artifact schema output. It rejects the special "." directory reference, enforces that artifact paths remain within the artifacts root, and flattens the get_artifact response from a wrapped field to individual metadata fields.

Changes

Artifact operation safety and schema restructuring

Layer / File(s) Summary
Path-safety validation gates
src/openhuman/artifacts/store.rs, src/openhuman/artifacts/store_tests.rs, src/openhuman/artifacts/ops.rs
validate_artifact_id rejects "." as an artifact ID; ai_get_artifact computes the artifacts root, resolves the artifact path, and returns an error if the resolved path does not remain within that root. A test verifies the "." rejection.
Schema output flattening
src/openhuman/artifacts/schemas.rs
schemas("get_artifact") output changes from a single opaque artifact wrapper field to individual flat fields (id, kind, title, path, size_bytes, status, created_at, absolute_path); schema test updated to assert the flat fields and confirm the wrapper is absent.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • tinyhumansai/openhuman#2801: Extends path-escape and artifact-ID validation safety in the same artifact RPC and filesystem persistence layer.

Suggested labels

rust-core, working

Poem

🐰 A path through the artifacts, safe and sound,
Escapes are caught before they can be found,
The schema flattens, cleaner and more bright,
Each field laid bare, a schema done right!
Validation guards the way, keeping us tight. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(artifacts): harden artifact ID validation and absolute_path' clearly and concisely summarizes the main security-focused changes across multiple files—validating artifact IDs, hardening path resolution, and correcting schema output.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@coderabbitai coderabbitai Bot added rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. working A PR that is being worked on by the team. labels May 28, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/artifacts/ops.rs`:
- Around line 64-73: The guard using Path::starts_with is insufficient because
it doesn't normalize `..`; update the check in the code around `artifacts_root`,
`resolved`, `meta.path`, and `absolute_path` to 1) reject absolute `meta.path`
inputs, and 2) reject any `..` components by iterating `meta.path.components()`
and returning an error if any Component::ParentDir is present (or if any
Component::Prefix/RootDir when Windows), then construct the final path as
`artifacts_root.join(&meta.path)` and proceed to create `absolute_path` (you may
still optionally canonicalize the final path if desired and it exists); this
prevents path traversal without relying on starts_with.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 62b6cbc5-fa97-4c40-bafc-3f71f450ddfb

📥 Commits

Reviewing files that changed from the base of the PR and between e5a83df and 1a59505.

📒 Files selected for processing (4)
  • src/openhuman/artifacts/ops.rs
  • src/openhuman/artifacts/schemas.rs
  • src/openhuman/artifacts/store.rs
  • src/openhuman/artifacts/store_tests.rs

Comment on lines +64 to +73
// Guard against a corrupt or adversarial meta.path that escapes the artifacts root.
let artifacts_root = config.workspace_dir.join("artifacts");
let resolved = artifacts_root.join(&meta.path);
if !resolved.starts_with(&artifacts_root) {
return Err(format!(
"[artifacts] meta.path {:?} escapes artifacts root for id={artifact_id}",
meta.path
));
}
let absolute_path = resolved.to_string_lossy().into_owned();
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Rust std::path, does Path::starts_with normalize .. path segments, or is it a lexical component-prefix check?

💡 Result:

In Rust, Path::starts_with performs a component-based, lexical check rather than a semantic or filesystem-aware normalization [1][2]. It does not normalize.. (parent directory) segments, nor does it resolve symbolic links [2][3]. Because it operates on path components, it strictly requires that the components of the base path match the leading components of the path being checked [1][4]. For example,.. segments are treated as literal components; therefore, a path like a/b/../c will not be treated as a/c, and thus would not match a prefix of a/c [2][5]. If you require normalization that resolves.. segments (lexically), you would need to use other utilities, such as experimental or third-party methods, or use Path::canonicalize if you intend to resolve paths against the actual filesystem [2][3][5].

Citations:


Fix path traversal guard in src/openhuman/artifacts/ops.rs

Path::starts_with is a component-based lexical prefix check and does not normalize/resolve .., so artifacts_root.join("../x") can still pass the starts_with test and produce an out-of-root absolute_path.

🔧 Suggested fix
-    let resolved = artifacts_root.join(&meta.path);
-    if !resolved.starts_with(&artifacts_root) {
+    let rel_path = std::path::Path::new(&meta.path);
+    if rel_path.is_absolute()
+        || rel_path
+            .components()
+            .any(|c| matches!(c, std::path::Component::ParentDir))
+    {
         return Err(format!(
             "[artifacts] meta.path {:?} escapes artifacts root for id={artifact_id}",
             meta.path
         ));
     }
-    let absolute_path = resolved.to_string_lossy().into_owned();
+    let absolute_path = artifacts_root.join(rel_path).to_string_lossy().into_owned();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Guard against a corrupt or adversarial meta.path that escapes the artifacts root.
let artifacts_root = config.workspace_dir.join("artifacts");
let resolved = artifacts_root.join(&meta.path);
if !resolved.starts_with(&artifacts_root) {
return Err(format!(
"[artifacts] meta.path {:?} escapes artifacts root for id={artifact_id}",
meta.path
));
}
let absolute_path = resolved.to_string_lossy().into_owned();
// Guard against a corrupt or adversarial meta.path that escapes the artifacts root.
let artifacts_root = config.workspace_dir.join("artifacts");
let rel_path = std::path::Path::new(&meta.path);
if rel_path.is_absolute()
|| rel_path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err(format!(
"[artifacts] meta.path {:?} escapes artifacts root for id={artifact_id}",
meta.path
));
}
let absolute_path = artifacts_root.join(rel_path).to_string_lossy().into_owned();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/artifacts/ops.rs` around lines 64 - 73, The guard using
Path::starts_with is insufficient because it doesn't normalize `..`; update the
check in the code around `artifacts_root`, `resolved`, `meta.path`, and
`absolute_path` to 1) reject absolute `meta.path` inputs, and 2) reject any `..`
components by iterating `meta.path.components()` and returning an error if any
Component::ParentDir is present (or if any Component::Prefix/RootDir when
Windows), then construct the final path as `artifacts_root.join(&meta.path)` and
proceed to create `absolute_path` (you may still optionally canonicalize the
final path if desired and it exists); this prevents path traversal without
relying on starts_with.

@M3gA-Mind M3gA-Mind merged commit c1869c6 into tinyhumansai:main May 28, 2026
35 of 38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant