Skip to content

feat: enhance filename sanitization and security checks to prevent path traversal attacks#111

Merged
aalemayhu merged 3 commits intomainfrom
security/2026.03.28
Mar 28, 2026
Merged

feat: enhance filename sanitization and security checks to prevent path traversal attacks#111
aalemayhu merged 3 commits intomainfrom
security/2026.03.28

Conversation

@aalemayhu
Copy link
Copy Markdown
Contributor

@aalemayhu aalemayhu commented Mar 28, 2026

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced file path validation to prevent path-traversal attacks during file operations, strengthening security when processing package files.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Warning

Rate limit exceeded

@aalemayhu has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 30 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 22 minutes and 30 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59796579-3ca0-4926-906b-f0c7dc01a412

📥 Commits

Reviewing files that changed from the base of the PR and between 599b253 and 000bd4b.

📒 Files selected for processing (1)
  • helpers/write_apkg.py
📝 Walkthrough

Walkthrough

A VS Code workspace configuration file was added, and the filename handling in write_apkg.py was enhanced with explicit path-traversal detection and basename-only constraints. Security tests were added to validate these protections against malicious filename inputs.

Changes

Cohort / File(s) Summary
Configuration
.vscode/settings.json
Added cSpell custom dictionary entries for Anki, apkg, and genanki.
Security Enhancement
helpers/write_apkg.py
Modified sanitize_filename to return only the basename component. Enhanced rename_temp_file with explicit path-traversal detection using os.path.realpath comparison and dangerous substring checks (.., /, \, :), with fallback rename protection included.
Security Tests
tests/test_write_apkg.py
Added test_sanitize_filename_security to validate path-traversal inputs are sanitized to safe, separator-free outputs. Added test_rename_temp_file_security and test_rename_temp_file_basename_only to verify rename_temp_file rejects dangerous inputs with ValueError and correctly processes safe inputs within the target directory.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Through filenames we hippity-hop,
With basenames only—no ..\ at the top!
Path traversal plots now stand no chance,
Our tests ensure safe file dance,
CodeRabbit's warren grows secure and wise,
No tricks will pass our rabbit eyes! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the changeset: enhancing filename sanitization and adding security checks to prevent path traversal attacks, which aligns with all three modified files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/2026.03.28

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 and usage tips.

Copy link
Copy Markdown

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
helpers/write_apkg.py (1)

77-144: ⚠️ Potential issue | 🟠 Major

rename_temp_file is over the local-variable threshold and duplicates safety logic.
pylint (too-many-locals) is failing. Extract path-validation/path-build into a small helper and reuse it for main + fallback branches to reduce locals and keep the safety checks consistent.

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

In `@helpers/write_apkg.py` around lines 77 - 144, The rename_temp_file function
has too many local variables and duplicates path-safety checks; extract a small
helper (e.g., build_safe_final_path or validate_and_build_path) that takes name
and deck_id (and optionally a fallback basename) and returns a validated
final_path (performing basename extraction, dangerous pattern check, length
truncation, os.path.join with cwd, realpath containment check, and final
basename strip), then call that helper for the main filename and again for the
fallback short_final_filename inside the OSError handler; update
rename_temp_file to use the helper to reduce locals and remove duplicated safety
logic while preserving the same exceptions and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@helpers/write_apkg.py`:
- Line 92: Remove trailing whitespace characters from the affected lines in
helpers/write_apkg.py (e.g., inside the write_apkg-related
functions/module-level code where blank/comment lines currently end with
spaces); strip the trailing spaces on those lines (and any other lines with
trailing whitespace in that file), save, and re-run lint to confirm pylint:
trailing-whitespace is resolved.
- Around line 133-137: When validating the fallback filename in the exception
handler, preserve the original OSError context by chaining the new ValueError to
the caught exception (variable e); locate the block that computes
real_final_path from final_path and checks startswith against real_cwd inside
the except OSError as e handler, and change the raise of ValueError("Path
traversal attempt detected in fallback filename") to raise that ValueError from
e so the traceback shows the original rename failure.
- Around line 121-122: Replace the brittle startswith containment check that
uses real_cwd + os.sep with an os.path.commonpath-based check using the existing
real_cwd and real_final_path variables; specifically, in the blocks around the
checks (the ones currently using real_final_path.startswith(real_cwd + os.sep)
and the identical check later), validate containment with if
os.path.commonpath([real_cwd, real_final_path]) != real_cwd: raise
ValueError("Path traversal attempt detected in final path"). Ensure real_cwd and
real_final_path are absolute/normalized beforehand (they already are in this
module) and apply the same replacement to both occurrences referenced in the
diff.

---

Outside diff comments:
In `@helpers/write_apkg.py`:
- Around line 77-144: The rename_temp_file function has too many local variables
and duplicates path-safety checks; extract a small helper (e.g.,
build_safe_final_path or validate_and_build_path) that takes name and deck_id
(and optionally a fallback basename) and returns a validated final_path
(performing basename extraction, dangerous pattern check, length truncation,
os.path.join with cwd, realpath containment check, and final basename strip),
then call that helper for the main filename and again for the fallback
short_final_filename inside the OSError handler; update rename_temp_file to use
the helper to reduce locals and remove duplicated safety logic while preserving
the same exceptions and behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1fb98004-64c3-42d2-9d4a-bac344da6f34

📥 Commits

Reviewing files that changed from the base of the PR and between e4783c3 and 599b253.

📒 Files selected for processing (3)
  • .vscode/settings.json
  • helpers/write_apkg.py
  • tests/test_write_apkg.py

Comment thread helpers/write_apkg.py Outdated
# Check for path traversal attempts in the original inputs
name_str = str(sanitized_name)
id_str = str(first_deck_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove trailing whitespace to unblock lint CI.
These lines currently fail the pipeline (pylint: trailing-whitespace).

Also applies to: 98-98, 102-102, 112-112, 116-116, 120-120, 132-132, 137-137

🧰 Tools
🪛 GitHub Actions: Lint

[error] 92-92: pylint (trailing-whitespace): Trailing whitespace

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

In `@helpers/write_apkg.py` at line 92, Remove trailing whitespace characters from
the affected lines in helpers/write_apkg.py (e.g., inside the write_apkg-related
functions/module-level code where blank/comment lines currently end with
spaces); strip the trailing spaces on those lines (and any other lines with
trailing whitespace in that file), save, and re-run lint to confirm pylint:
trailing-whitespace is resolved.

Comment thread helpers/write_apkg.py Outdated
Comment on lines +121 to +122
if not real_final_path.startswith(real_cwd + os.sep) and not real_final_path == real_cwd:
raise ValueError("Path traversal attempt detected in final path")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n helpers/write_apkg.py | head -150

Repository: 2anki/create_deck

Length of output: 6088


🏁 Script executed:

sed -n '110,140p' helpers/write_apkg.py

Repository: 2anki/create_deck

Length of output: 1526


🏁 Script executed:

sed -n '90,130p' helpers/write_apkg.py

Repository: 2anki/create_deck

Length of output: 1779


🏁 Script executed:

python3 << 'EOF'
import os
import tempfile

# Test the original logic with root directory
real_cwd = "/"
test_paths = [
    "/tmp/file.txt",      # Valid: under root
    "/home/user/file.txt", # Valid: under root
    "/",                   # Valid: the root itself
    "/../etc/passwd",      # Should reject traversal
]

print("Testing original logic: startswith(real_cwd + os.sep) and real_final_path == real_cwd")
print(f"real_cwd = '{real_cwd}'")
print(f"real_cwd + os.sep = '{real_cwd + os.sep}'")
print()

for path in test_paths:
    # Normalize path like the code would
    real_final_path = os.path.realpath(path)
    original_check = not real_final_path.startswith(real_cwd + os.sep) and not real_final_path == real_cwd
    print(f"Path: {path}")
    print(f"  Resolved to: {real_final_path}")
    print(f"  Original logic rejects: {original_check}")
    print()

print("\n" + "="*60)
print("Testing suggested fix: os.path.commonpath([real_cwd, real_final_path]) != real_cwd")
print()

for path in test_paths:
    real_final_path = os.path.realpath(path)
    suggested_check = os.path.commonpath([real_cwd, real_final_path]) != real_cwd
    print(f"Path: {path}")
    print(f"  Resolved to: {real_final_path}")
    print(f"  Suggested logic rejects: {suggested_check}")
    print()

print("\n" + "="*60)
print("Testing with non-root CWD:")
real_cwd = "/home/user"
print(f"real_cwd = '{real_cwd}'")
print(f"real_cwd + os.sep = '{real_cwd + os.sep}'")
print()

test_paths_nonroot = [
    "/home/user/file.txt",      # Valid
    "/home/user",               # Valid: the cwd itself
    "/home/other/file.txt",     # Invalid: outside cwd
]

for path in test_paths_nonroot:
    real_final_path = os.path.realpath(path)
    original_check = not real_final_path.startswith(real_cwd + os.sep) and not real_final_path == real_cwd
    suggested_check = os.path.commonpath([real_cwd, real_final_path]) != real_cwd
    print(f"Path: {path}")
    print(f"  Resolved to: {real_final_path}")
    print(f"  Original rejects: {original_check}, Suggested rejects: {suggested_check}")
    print()
EOF

Repository: 2anki/create_deck

Length of output: 1436


🏁 Script executed:

python3 << 'EOF'
import os

# Check potential issues with commonpath
print("Testing edge cases with os.path.commonpath:")
print()

# Case 1: Symlink escapes
print("Case 1: Path with similar prefix but different meaning")
paths_to_test = [
    ("/home/user", "/home/user_backup"),
    ("/home/user", "/home/username"),
    ("/app", "/apple"),
]

for cwd, final in paths_to_test:
    common = os.path.commonpath([cwd, final])
    check = common != cwd
    print(f"CWD: {cwd}, Path: {final}")
    print(f"  Common path: {common}")
    print(f"  Would reject: {check}")
    print()

# Case 2: Root directory edge case
print("Case 2: Root directory behavior")
print(f"os.path.commonpath(['/'], '/tmp/file'): {os.path.commonpath(['/'], '/tmp/file')}")
print(f"os.path.commonpath(['/'], '/'): {os.path.commonpath(['/'], '/')}")
print()

# Case 3: What if paths are on different drives (Windows)?
# This is less relevant on Linux but good to know
print("Case 3: Non-existent paths")
try:
    result = os.path.commonpath(["/nonexistent", "/nonexistent/file"])
    print(f"commonpath with non-existent paths works: {result}")
except Exception as e:
    print(f"commonpath with non-existent paths fails: {e}")
EOF

Repository: 2anki/create_deck

Length of output: 580


Path containment check fails when CWD is root (/).

When real_cwd is /, the expression real_cwd + os.sep produces //, causing startswith() to incorrectly reject all valid paths like /tmp/file.txt. Use os.path.commonpath([real_cwd, real_final_path]) != real_cwd instead, which correctly validates path containment and also prevents prefix-matching attacks (e.g., distinguishing /home/user from /home/user_backup).

Suggested fix
-    if not real_final_path.startswith(real_cwd + os.sep) and not real_final_path == real_cwd:
+    if os.path.commonpath([real_cwd, real_final_path]) != real_cwd:
         raise ValueError("Path traversal attempt detected in final path")
...
-            if not real_final_path.startswith(real_cwd + os.sep) and not real_final_path == real_cwd:
+            if os.path.commonpath([real_cwd, real_final_path]) != real_cwd:
                 raise ValueError("Path traversal attempt detected in fallback filename")

Applies to lines 121-122 and 135-136.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 121-121: Use the opposite operator ("!=") instead.

See more on https://sonarcloud.io/project/issues?id=2anki_create_deck&issues=AZ00e4OYPAYx8a-QSfKf&open=AZ00e4OYPAYx8a-QSfKf&pullRequest=111

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

In `@helpers/write_apkg.py` around lines 121 - 122, Replace the brittle startswith
containment check that uses real_cwd + os.sep with an os.path.commonpath-based
check using the existing real_cwd and real_final_path variables; specifically,
in the blocks around the checks (the ones currently using
real_final_path.startswith(real_cwd + os.sep) and the identical check later),
validate containment with if os.path.commonpath([real_cwd, real_final_path]) !=
real_cwd: raise ValueError("Path traversal attempt detected in final path").
Ensure real_cwd and real_final_path are absolute/normalized beforehand (they
already are in this module) and apply the same replacement to both occurrences
referenced in the diff.

Comment thread helpers/write_apkg.py Outdated
@sonarqubecloud
Copy link
Copy Markdown

@aalemayhu aalemayhu merged commit 7e31529 into main Mar 28, 2026
9 checks passed
@aalemayhu aalemayhu deleted the security/2026.03.28 branch March 28, 2026 12:58
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.

1 participant