Skip to content

Reject control characters in decoded filenames#304

Open
nitrobass24 wants to merge 3 commits intodevelopfrom
fix/reject-control-chars-filename
Open

Reject control characters in decoded filenames#304
nitrobass24 wants to merge 3 commits intodevelopfrom
fix/reject-control-chars-filename

Conversation

@nitrobass24
Copy link
Owner

@nitrobass24 nitrobass24 commented Mar 19, 2026

Summary

  • Add validation in _validate_filename() to reject ASCII control characters (0x00-0x1F, 0x7F) in filenames, preventing shell command injection and log injection via CR, LF, tab, and other control characters
  • Add 7 integration tests covering control character rejection, normal filename acceptance, and path traversal rejection

Test plan

  • ruff check and ruff format --check pass on controller.py
  • pyright passes with 0 errors
  • All 7 new tests pass: newline, carriage return, tab, SOH (0x01), DEL (0x7F) rejected; normal filenames accepted; path traversal still rejected
  • 5 pre-existing test failures in TestControllerHandler (absolute path /value/with/slashes rejected by os.path.isabs) are unrelated to this change

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced filename validation to reject filenames containing control characters and path traversal attempts.
  • Tests

    • Added comprehensive test coverage for filename validation across edge cases and invalid input patterns.

Add validation to reject ASCII control characters (0x00-0x1F, 0x7F)
in filenames to prevent shell command injection and log injection
attacks via CR, LF, tab, and other control characters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

Warning

Rate limit exceeded

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

⌛ 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6eb34bed-bc5e-4d7b-b05c-99a9778d7fce

📥 Commits

Reviewing files that changed from the base of the PR and between 5d3a352 and 3054779.

📒 Files selected for processing (1)
  • src/python/tests/integration/test_web/test_handler/test_controller.py
📝 Walkthrough

Walkthrough

This PR extends filename validation in the controller handler by rejecting filenames containing control characters (Unicode codepoints 0x00–0x1F and 0x7F), complemented by seven new test cases covering control characters, normal filenames, and path traversal attempts.

Changes

Cohort / File(s) Summary
Validation Logic
src/python/web/handler/controller.py
Extended _validate_filename to reject filenames containing control characters in ranges 0x00–0x1F and 0x7F.
Validation Tests
src/python/tests/integration/test_web/test_handler/test_controller.py
Added TestControllerHandlerValidation class with seven test methods: control character rejection tests (newline, carriage return, tab, SOH, DEL), normal filename acceptance, and path traversal rejection.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

Poem

🐰 Hop along, files so clean,
No naughty control chars in between,
Newlines and tabs shall not pass,
Our validation is built to last!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.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 'Reject control characters in decoded filenames' directly and clearly summarizes the main change: updating filename validation to reject ASCII control characters to prevent injection attacks.

✏️ 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 fix/reject-control-chars-filename
📝 Coding Plan
  • Generate coding plan for human review comments

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

@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 the current code and only fix it if needed.

Inline comments:
In `@src/python/tests/integration/test_web/test_handler/test_controller.py`:
- Around line 195-218: Each control-char rejection test
(test_control_char_newline_rejected, test_control_char_carriage_return_rejected,
test_control_char_tab_rejected, test_control_char_soh_rejected,
test_control_char_del_rejected) should not only assert a 400 response but also
assert that the mocked dispatcher was not invoked; after each
self.test_app.get(...) add queue_command.assert_not_called() (using the existing
mock named queue_command in the test) to prove no command was queued for invalid
filenames—apply the same addition to the other similar rejection tests in this
file as well.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d1d75cbb-d3df-4cb2-9662-0bc420e30147

📥 Commits

Reviewing files that changed from the base of the PR and between e7b5577 and 5d3a352.

📒 Files selected for processing (2)
  • src/python/tests/integration/test_web/test_handler/test_controller.py
  • src/python/web/handler/controller.py

Comment on lines +195 to +218
def test_control_char_newline_rejected(self):
uri = quote(quote("file\nname", safe=""), safe="")
resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
self.assertEqual(400, resp.status_int)

def test_control_char_carriage_return_rejected(self):
uri = quote(quote("file\rname", safe=""), safe="")
resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
self.assertEqual(400, resp.status_int)

def test_control_char_tab_rejected(self):
uri = quote(quote("file\tname", safe=""), safe="")
resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
self.assertEqual(400, resp.status_int)

def test_control_char_soh_rejected(self):
uri = quote(quote("file\x01name", safe=""), safe="")
resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
self.assertEqual(400, resp.status_int)

def test_control_char_del_rejected(self):
uri = quote(quote("file\x7fname", safe=""), safe="")
resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
self.assertEqual(400, resp.status_int)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Strengthen rejection tests by asserting no command dispatch on invalid filenames.

Right now these tests only assert 400. Add queue_command.assert_not_called() so they also prove fail-closed behavior (no side effects) for rejected inputs.

Proposed test tightening
 class TestControllerHandlerValidation(BaseTestWebApp):
     """Tests for filename validation in controller handler."""

+    def _assert_queue_rejected(self, raw_filename: str):
+        self.controller.queue_command = MagicMock()
+        uri = quote(quote(raw_filename, safe=""), safe="")
+        resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
+        self.assertEqual(400, resp.status_int)
+        self.controller.queue_command.assert_not_called()
+
     def test_control_char_newline_rejected(self):
-        uri = quote(quote("file\nname", safe=""), safe="")
-        resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
-        self.assertEqual(400, resp.status_int)
+        self._assert_queue_rejected("file\nname")

     def test_control_char_carriage_return_rejected(self):
-        uri = quote(quote("file\rname", safe=""), safe="")
-        resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
-        self.assertEqual(400, resp.status_int)
+        self._assert_queue_rejected("file\rname")

     def test_control_char_tab_rejected(self):
-        uri = quote(quote("file\tname", safe=""), safe="")
-        resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
-        self.assertEqual(400, resp.status_int)
+        self._assert_queue_rejected("file\tname")

     def test_control_char_soh_rejected(self):
-        uri = quote(quote("file\x01name", safe=""), safe="")
-        resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
-        self.assertEqual(400, resp.status_int)
+        self._assert_queue_rejected("file\x01name")

     def test_control_char_del_rejected(self):
-        uri = quote(quote("file\x7fname", safe=""), safe="")
-        resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
-        self.assertEqual(400, resp.status_int)
+        self._assert_queue_rejected("file\x7fname")

     def test_path_traversal_rejected(self):
-        uri = quote(quote("../etc/passwd", safe=""), safe="")
-        resp = self.test_app.get("/server/command/queue/" + uri, expect_errors=True)
-        self.assertEqual(400, resp.status_int)
+        self._assert_queue_rejected("../etc/passwd")

Also applies to: 232-235

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

In `@src/python/tests/integration/test_web/test_handler/test_controller.py` around
lines 195 - 218, Each control-char rejection test
(test_control_char_newline_rejected, test_control_char_carriage_return_rejected,
test_control_char_tab_rejected, test_control_char_soh_rejected,
test_control_char_del_rejected) should not only assert a 400 response but also
assert that the mocked dispatcher was not invoked; after each
self.test_app.get(...) add queue_command.assert_not_called() (using the existing
mock named queue_command in the test) to prove no command was queued for invalid
filenames—apply the same addition to the other similar rejection tests in this
file as well.

nitrobass24 and others added 2 commits March 19, 2026 22:32
Verify that invalid filenames are rejected before reaching the
controller by adding queue_command.assert_not_called() to all
control character rejection tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix 5 pre-existing test failures: change test data from absolute path
  "/value/with/slashes" to relative "value/with/slashes" — the path
  validation correctly rejects absolute paths, the tests were outdated
- Add queue_command.assert_not_called() to all control char rejection
  tests to verify invalid filenames never reach the controller

All 12 tests now pass (5 pre-existing + 7 new).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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