Skip to content

Potential fix for code scanning alert no. 142: Uncontrolled data used in path expression#630

Closed
schneidergithub wants to merge 3 commits intomainfrom
alert-autofix-142
Closed

Potential fix for code scanning alert no. 142: Uncontrolled data used in path expression#630
schneidergithub wants to merge 3 commits intomainfrom
alert-autofix-142

Conversation

@schneidergithub
Copy link
Collaborator

@schneidergithub schneidergithub commented Feb 18, 2026

Potential fix for https://github.com/ace-step/ACE-Step-1.5/security/code-scanning/142

In general, paths derived from untrusted input should either be (a) normalized and checked to ensure they reside under a trusted root directory, or (b) reduced to safe filenames (e.g., using secure_filename) and combined with a trusted root. For an export endpoint like this, the safest design is to constrain exports to a configured base directory, then ensure the user-supplied portion cannot escape that base (no absolute paths, no .. up-traversals).

For this specific function, the least invasive fix while preserving behavior is:

  1. Introduce a fixed base directory for exports (e.g., ./exports or whatever is appropriate for this service).
  2. Normalize the user-supplied export_path and join it to that base directory.
  3. Ensure the resulting full path is still under the base directory, rejecting any path that tries to escape it.
  4. Use that validated export_path for all filesystem operations (os.makedirs, shutil.rmtree, shutil.copytree).

Concretely, within acestep/api/train_api_service.py:

  • After computing export_path = request.export_path.strip(), compute a safe base like safe_base = os.path.abspath("./exports") (or another sensible root), then compute export_path = os.path.abspath(os.path.join(safe_base, export_path)).
  • Check if not export_path.startswith(safe_base + os.sep): (or equivalent robust prefix check) and raise HTTPException(status_code=400, detail="Invalid export path") if the check fails.
  • Keep the rest of the logic (creating the directory if needed, removing an existing directory, copying from source_path) unchanged, but now using the validated export_path.

This adds only local logic in the existing function, uses already-imported os, and does not change the API surface beyond rejecting malicious paths.


Suggested fixes powered by Copilot Autofix. Review carefully before merging.

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened export security by sanitizing and validating export targets to block path traversal and restrict exports to a designated safe directory.
    • Ensured required export directories are created automatically and preserved existing export behavior (replace target with source) after validation.
    • Improved error handling so original HTTP errors are preserved and unexpected failures return a clear server error.

… in path expression

If running locally as root or super-user (your own computer), this is not a big deal.  However, on restricted computers, this is a security risk and considered best practice.

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Replaces export path handling with a safe-base constrained strategy: input paths are sanitized, resolved under "./exports", required to start with that base, and directories are created. Invalid paths raise HTTP 400. HTTPException is re-raised; other exceptions return HTTP 500. Existing export (remove/copy) logic preserved.

Changes

Cohort / File(s) Summary
Export path & error handling
acestep/api/train_api_service.py
Normalize input export path, remove leading separators, join with safe base ./exports, ensure resulting path is inside the safe base (or equal), create directories as needed. Preserve mkdir/remove/copy export workflow. Re-raise HTTPException to keep status codes; return 500 for other errors.

Sequence Diagram(s)

(omitted — changes are localized and do not introduce multi-component sequential flows)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • ChuxiJ

Poem

🐰 I tidy paths with careful hops,
Trimming slashes, guarding drops.
In "./exports" my treasures stay,
Safe and snug by night and day.
🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: fixing code scanning alert #142 by implementing uncontrolled data path validation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch alert-autofix-142

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

@schneidergithub schneidergithub marked this pull request as ready for review February 18, 2026 02:16
Copilot AI review requested due to automatic review settings February 18, 2026 02:16
Copy link
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

🧹 Nitpick comments (1)
acestep/api/train_api_service.py (1)

183-183: Ensure the base exports directory exists.

The safe_base directory (./exports) may not exist. While os.makedirs on line 183 creates parent directories of export_path, if exports itself doesn't exist and export_path has no subdirectory component, the operation could fail or behave unexpectedly.

🛠️ Proposed fix to ensure base directory exists
             safe_base = os.path.abspath("./exports")
+            os.makedirs(safe_base, exist_ok=True)
             export_path = export_path.lstrip("/").lstrip("\\")
             export_path = os.path.abspath(os.path.join(safe_base, export_path))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/api/train_api_service.py` at line 183, The code currently calls
os.makedirs(os.path.dirname(export_path) if os.path.dirname(export_path) else
".", exist_ok=True) which doesn't guarantee the base exports directory
(safe_base, e.g. "./exports") exists; update the logic to explicitly ensure
safe_base exists before creating parent dirs for export_path by calling
os.makedirs(safe_base, exist_ok=True) (or equivalent) prior to the existing
os.makedirs line, referencing the export_path and safe_base variables so the
exports base folder is always created first.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@acestep/api/train_api_service.py`:
- Around line 177-181: The export_path containment check is bypassable because
os.path.join(safe_base, export_path) will treat an absolute export_path as
replacing safe_base; before joining, sanitize the user-supplied export_path (the
variable export_path used in this snippet) by stripping any leading path
separators or drive letters (e.g., remove leading "/" or "\" and on Windows
strip "C:"), then compute export_path = os.path.abspath(os.path.join(safe_base,
sanitized_export_path)) and keep the existing containment check that raises
HTTPException(status_code=400, detail="Invalid export path") if the resolved
path is outside safe_base.

---

Nitpick comments:
In `@acestep/api/train_api_service.py`:
- Line 183: The code currently calls os.makedirs(os.path.dirname(export_path) if
os.path.dirname(export_path) else ".", exist_ok=True) which doesn't guarantee
the base exports directory (safe_base, e.g. "./exports") exists; update the
logic to explicitly ensure safe_base exists before creating parent dirs for
export_path by calling os.makedirs(safe_base, exist_ok=True) (or equivalent)
prior to the existing os.makedirs line, referencing the export_path and
safe_base variables so the exports base folder is always created first.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a code scanning alert (#142) for an uncontrolled path expression vulnerability in the export_lora API endpoint by introducing path validation logic to prevent path traversal attacks. The fix adds a safe base directory constraint and validates that user-supplied paths cannot escape this base using absolute paths or .. traversals.

Changes:

  • Added path validation to constrain export_path to a ./exports base directory
  • Added exception handling to re-raise HTTP exceptions while catching other errors

schneidergithub and others added 2 commits February 17, 2026 21:20
First commit was too focused, this will fix another alert too.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
good catch

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
if not export_path.startswith(safe_base + os.sep) and export_path != safe_base:
raise HTTPException(status_code=400, detail="Invalid export path")

os.makedirs(os.path.dirname(export_path), exist_ok=True)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

In general, to fix uncontrolled-path issues, normalize the path relative to a trusted base directory and then verify that the normalized path is still within that base. Use os.path.abspath/os.path.normpath or os.path.realpath, and compare using robust directory containment checks (e.g., os.path.commonpath) instead of plain string-prefix checks, which can be error‑prone (e.g., /exports2 vs /exports).

In this specific function (export_lora in acestep/api/train_api_service.py), we should: (1) keep treating ./exports as the only allowed root; (2) normalize the user-provided request.export_path relative to safe_base; (3) verify containment using os.path.commonpath([safe_base, export_path]) == safe_base; and (4) optionally handle the case where export_path is empty or only whitespace. After we have a validated export_path, we can safely call os.path.dirname(export_path) and the write operations (os.makedirs, shutil.copytree) without risking writes outside safe_base. We can implement this entirely within the shown snippet: no new imports are needed, as os is already imported, and we only adjust the logic around lines 175–183 to strengthen validation while leaving the external behavior (export under ./exports/...) the same.

Concretely:

  • In the try block of export_lora, replace the current sanitization code (lines 176–183) with a more robust version:
    • Strip and check that request.export_path is non-empty.
    • Strip leading slashes/backslashes.
    • Build export_path with os.path.join(safe_base, user_path) and normalize with os.path.abspath.
    • Use os.path.commonpath([safe_base, export_path]) to ensure export_path stays under safe_base.
  • Keep the rest of the logic (creating parent directory, removing existing tree, copying) unchanged.

This hardens the path validation and should eliminate the CodeQL alert because all sinks now consume a value that has passed a strict containment check.


Suggested changeset 1
acestep/api/train_api_service.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/acestep/api/train_api_service.py b/acestep/api/train_api_service.py
--- a/acestep/api/train_api_service.py
+++ b/acestep/api/train_api_service.py
@@ -173,20 +173,27 @@
             raise HTTPException(status_code=404, detail=f"No trained model found in {request.lora_output_dir}")
 
         try:
-            export_path = request.export_path.strip()
+            raw_export_path = (request.export_path or "").strip()
+            if not raw_export_path:
+                raise HTTPException(status_code=400, detail="Export path must not be empty")
+
             # Constrain export_path to a safe base directory and prevent path traversal
             safe_base = os.path.abspath("./exports")
             # Strip leading separators to prevent os.path.join from treating input as absolute
-            export_path = export_path.lstrip("/").lstrip("\\")
-            export_path = os.path.abspath(os.path.join(safe_base, export_path))
-            if not export_path.startswith(safe_base + os.sep) and export_path != safe_base:
+            sanitized_relative = raw_export_path.lstrip("/").lstrip("\\")
+            export_path = os.path.abspath(os.path.join(safe_base, sanitized_relative))
+
+            # Ensure the final path is within the safe base directory
+            if os.path.commonpath([safe_base, export_path]) != safe_base:
                 raise HTTPException(status_code=400, detail="Invalid export path")
 
             os.makedirs(os.path.dirname(export_path), exist_ok=True)
             if os.path.exists(export_path):
                 shutil.rmtree(export_path)
             shutil.copytree(source_path, export_path)
-            return wrap_response({"message": "LoRA exported successfully", "export_path": export_path, "source": source_path})
+            return wrap_response(
+                {"message": "LoRA exported successfully", "export_path": export_path, "source": source_path}
+            )
         except HTTPException:
             # Re-raise HTTP exceptions to preserve intended status codes
             raise
EOF
@@ -173,20 +173,27 @@
raise HTTPException(status_code=404, detail=f"No trained model found in {request.lora_output_dir}")

try:
export_path = request.export_path.strip()
raw_export_path = (request.export_path or "").strip()
if not raw_export_path:
raise HTTPException(status_code=400, detail="Export path must not be empty")

# Constrain export_path to a safe base directory and prevent path traversal
safe_base = os.path.abspath("./exports")
# Strip leading separators to prevent os.path.join from treating input as absolute
export_path = export_path.lstrip("/").lstrip("\\")
export_path = os.path.abspath(os.path.join(safe_base, export_path))
if not export_path.startswith(safe_base + os.sep) and export_path != safe_base:
sanitized_relative = raw_export_path.lstrip("/").lstrip("\\")
export_path = os.path.abspath(os.path.join(safe_base, sanitized_relative))

# Ensure the final path is within the safe base directory
if os.path.commonpath([safe_base, export_path]) != safe_base:
raise HTTPException(status_code=400, detail="Invalid export path")

os.makedirs(os.path.dirname(export_path), exist_ok=True)
if os.path.exists(export_path):
shutil.rmtree(export_path)
shutil.copytree(source_path, export_path)
return wrap_response({"message": "LoRA exported successfully", "export_path": export_path, "source": source_path})
return wrap_response(
{"message": "LoRA exported successfully", "export_path": export_path, "source": source_path}
)
except HTTPException:
# Re-raise HTTP exceptions to preserve intended status codes
raise
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
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.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@acestep/api/train_api_service.py`:
- Around line 176-181: The current check still allows absolute paths because
os.path.join(safe_base, export_path) is bypassed when request.export_path is
absolute; fix by normalizing and forcing export_path to be relative before
joining: take request.export_path, strip any leading path separators (e.g.,
export_path = request.export_path.lstrip(os.sep) and on Windows also
lstrip("\\/") or use a cross-platform lstrip for both slashes), then
os.path.normpath that result, join with safe_base, and validate as before (using
safe_base and export_path variables and raising HTTPException on failure).
Ensure you update the code paths that set export_path and preserve the existing
safe_base and HTTPException usage.

@schneidergithub schneidergithub marked this pull request as draft February 18, 2026 02:25
@schneidergithub
Copy link
Collaborator Author

@ChuxiJ - I am converting this to a draft. The auto-suggested fix from the security scanner is not understanding the full architecture. I haven't tested this fix out, but suspect it could break based on whether it's a windows or mac/linux machine.

Regardless, these security warnings are typically meant in stricter work environments and less concerning on personal computers. Up to you if this security issue is a high priority.

Copy link
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
acestep/api/train_api_service.py (1)

158-160: ⚠️ Potential issue | 🟡 Minor

Add return type + expand the docstring to include inputs/outputs/exceptions.

The function body was modified, so please align the docstring and type hints with the repo’s Python guidelines.

🔧 Suggested update
-    async def export_lora(request: ExportLoRARequest, _: None = Depends(verify_api_key)):
-        """Export trained LoRA/LoKr weights."""
+    async def export_lora(
+        request: ExportLoRARequest, _: None = Depends(verify_api_key)
+    ) -> Dict[str, Any]:
+        """Export trained LoRA/LoKr weights.
+
+        Args:
+            request: Export parameters including `export_path` and `lora_output_dir`.
+        Returns:
+            Response payload with export metadata.
+        Raises:
+            HTTPException: If no model is found or the export path is invalid.
+        """

As per coding guidelines: "Docstrings must be concise and include purpose plus key inputs/outputs (and raised exceptions when relevant)" and "Add type hints for new/modified functions when practical in Python."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/api/train_api_service.py` around lines 158 - 160, The export_lora
endpoint's signature and docstring are missing a return type and detailed
docstring; update the async def export_lora(request: ExportLoRARequest, _: None
= Depends(verify_api_key)) to include an explicit return type (e.g., -> Response
or the specific pydantic/fastapi response model used) and expand its docstring
to briefly describe purpose, parameters (request: ExportLoRARequest, dependency:
verify_api_key), the returned value (type and shape), and any exceptions/errors
that can be raised (e.g., HTTPException for auth or export failures); ensure the
docstring follows module style (concise purpose + inputs/outputs/exceptions) and
keep the function name export_lora and request type ExportLoRARequest as
references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@acestep/api/train_api_service.py`:
- Around line 158-160: The export_lora endpoint's signature and docstring are
missing a return type and detailed docstring; update the async def
export_lora(request: ExportLoRARequest, _: None = Depends(verify_api_key)) to
include an explicit return type (e.g., -> Response or the specific
pydantic/fastapi response model used) and expand its docstring to briefly
describe purpose, parameters (request: ExportLoRARequest, dependency:
verify_api_key), the returned value (type and shape), and any exceptions/errors
that can be raised (e.g., HTTPException for auth or export failures); ensure the
docstring follows module style (concise purpose + inputs/outputs/exceptions) and
keep the function name export_lora and request type ExportLoRARequest as
references.

@ChuxiJ
Copy link
Contributor

ChuxiJ commented Feb 18, 2026

@ChuxiJ - I am converting this to a draft. The auto-suggested fix from the security scanner is not understanding the full architecture. I haven't tested this fix out, but suspect it could break based on whether it's a windows or mac/linux machine.

Regardless, these security warnings are typically meant in stricter work environments and less concerning on personal computers. Up to you if this security issue is a high priority.

Thanks for pointing this out.
For now, we’re prioritizing local execution; service deployment is not high on the list.
But we will need to systematically address these issues later on.

@schneidergithub
Copy link
Collaborator Author

Closing PR and dismissing security alerts. Everyone is aware of the issue and planning for a fix in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments