🛡️ Sentinel: [HIGH] Validate rule input to prevent XSS#117
Conversation
Validates rule content from external JSON lists to ensure they do not contain malicious characters (XSS payloads, control characters) before pushing to Control D API. Filters out unsafe rules and logs a warning. Fixes potential Stored XSS vulnerability via malicious blocklists.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
Merging to
|
| ) | ||
|
|
||
| # Check what was sent | ||
| assert mock_post_form.called |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| sent_rules.append(v) | ||
|
|
||
| # EXPECTED BEHAVIOR: Malicious rules are NOT sent | ||
| assert "<script>alert(1)</script>" not in sent_rules |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
|
|
||
| # EXPECTED BEHAVIOR: Malicious rules are NOT sent | ||
| assert "<script>alert(1)</script>" not in sent_rules | ||
| assert "javascript:void(0)" not in sent_rules # Contains parenthesis/colon? |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| assert "<script>alert(1)</script>" not in sent_rules | ||
| assert "javascript:void(0)" not in sent_rules # Contains parenthesis/colon? | ||
| # Wait, 'javascript:void(0)' has '('. My validator blocks '('. | ||
| assert "fail' OR '1'='1" not in sent_rules # Contains ' |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| assert "javascript:void(0)" not in sent_rules # Contains parenthesis/colon? | ||
| # Wait, 'javascript:void(0)' has '('. My validator blocks '('. | ||
| assert "fail' OR '1'='1" not in sent_rules # Contains ' | ||
| assert "img src=x onerror=alert(1)" not in sent_rules # Contains ( ) or =? No = is allowed? |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
|
|
||
| # Valid rules MUST be sent | ||
| assert "valid.com" in sent_rules | ||
| assert "safe-domain.com" in sent_rules |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| # Valid rules MUST be sent | ||
| assert "valid.com" in sent_rules | ||
| assert "safe-domain.com" in sent_rules | ||
| assert "1.1.1.1" in sent_rules |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| assert "valid.com" in sent_rules | ||
| assert "safe-domain.com" in sent_rules | ||
| assert "1.1.1.1" in sent_rules | ||
| assert "*.wildcard.com" in sent_rules |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
|
|
||
| # Check logs for warnings | ||
| # We expect 4 skipped rules | ||
| assert mock_log.warning.call_count >= 1 |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| for call in mock_log.warning.call_args_list: | ||
| if "Skipping unsafe rule" in str(call): | ||
| found_unsafe_log = True | ||
| assert found_unsafe_log |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| **Prevention:** | ||
| 1. Always use `if not ip.is_global or ip.is_multicast:` for strict SSRF filtering, rather than manual blacklists of private ranges. | ||
|
|
||
| ## 2026-05-14 - [Indirect XSS via Third-Party Data] |
Check notice
Code scanning / Remark-lint (reported by Codacy)
Warn when shortcut reference links are used. Note
| **Prevention:** | ||
| 1. Always use `if not ip.is_global or ip.is_multicast:` for strict SSRF filtering, rather than manual blacklists of private ranges. | ||
|
|
||
| ## 2026-05-14 - [Indirect XSS via Third-Party Data] |
Check notice
Code scanning / Remark-lint (reported by Codacy)
Warn when references to undefined definitions are found. Note
|
|
||
| for h in hostnames: | ||
| if not is_valid_rule(h): | ||
| log.warning(f"Skipping unsafe rule in {sanitize_for_log(folder_name)}: {sanitize_for_log(h)}") |
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
|
|
||
| duplicates_count = original_count - len(filtered_hostnames) | ||
| if skipped_unsafe > 0: | ||
| log.warning(f"Folder {sanitize_for_log(folder_name)}: skipped {skipped_unsafe} unsafe rules") |
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
| @@ -0,0 +1,87 @@ | |||
| import pytest | |||
Check warning
Code scanning / Prospector (reported by Codacy)
Unable to import 'pytest' (import-error) Warning test
| @@ -0,0 +1,87 @@ | |||
| import pytest | |||
Check warning
Code scanning / Prospector (reported by Codacy)
Unable to import 'pytest' (import-error) Warning test
| mock_post_form = MagicMock() | ||
|
|
||
| # Patch the API call | ||
| original_post_form = main._api_post_form |
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _api_post_form of a client class (protected-access) Warning test
|
|
||
| # Patch the API call | ||
| original_post_form = main._api_post_form | ||
| main._api_post_form = mock_post_form |
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _api_post_form of a client class (protected-access) Warning test
|
|
||
| sent_rules = [] | ||
| for call in calls: | ||
| args, kwargs = call |
Check warning
Code scanning / Prospector (reported by Codacy)
Unused variable 'args' (unused-variable) Warning test
| assert found_unsafe_log | ||
|
|
||
| finally: | ||
| main._api_post_form = original_post_form |
Check warning
Code scanning / Prospector (reported by Codacy)
Access to a protected member _api_post_form of a client class (protected-access) Warning test
| ) | ||
|
|
||
| # Check what was sent | ||
| assert mock_post_form.called |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| sent_rules.append(v) | ||
|
|
||
| # EXPECTED BEHAVIOR: Malicious rules are NOT sent | ||
| assert "<script>alert(1)</script>" not in sent_rules |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
|
|
||
| # EXPECTED BEHAVIOR: Malicious rules are NOT sent | ||
| assert "<script>alert(1)</script>" not in sent_rules | ||
| assert "javascript:void(0)" not in sent_rules # Contains parenthesis/colon? |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| assert "<script>alert(1)</script>" not in sent_rules | ||
| assert "javascript:void(0)" not in sent_rules # Contains parenthesis/colon? | ||
| # Wait, 'javascript:void(0)' has '('. My validator blocks '('. | ||
| assert "fail' OR '1'='1" not in sent_rules # Contains ' |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| assert "javascript:void(0)" not in sent_rules # Contains parenthesis/colon? | ||
| # Wait, 'javascript:void(0)' has '('. My validator blocks '('. | ||
| assert "fail' OR '1'='1" not in sent_rules # Contains ' | ||
| assert "img src=x onerror=alert(1)" not in sent_rules # Contains ( ) or =? No = is allowed? |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
|
|
||
| # Valid rules MUST be sent | ||
| assert "valid.com" in sent_rules | ||
| assert "safe-domain.com" in sent_rules |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| # Valid rules MUST be sent | ||
| assert "valid.com" in sent_rules | ||
| assert "safe-domain.com" in sent_rules | ||
| assert "1.1.1.1" in sent_rules |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| assert "valid.com" in sent_rules | ||
| assert "safe-domain.com" in sent_rules | ||
| assert "1.1.1.1" in sent_rules | ||
| assert "*.wildcard.com" in sent_rules |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
|
|
||
| # Check logs for warnings | ||
| # We expect 4 skipped rules | ||
| assert mock_log.warning.call_count >= 1 |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| for call in mock_log.warning.call_args_list: | ||
| if "Skipping unsafe rule" in str(call): | ||
| found_unsafe_log = True | ||
| assert found_unsafe_log |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
|
|
||
| for h in hostnames: | ||
| if not is_valid_rule(h): | ||
| log.warning(f"Skipping unsafe rule in {sanitize_for_log(folder_name)}: {sanitize_for_log(h)}") |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (106/100) Warning
|
|
||
| duplicates_count = original_count - len(filtered_hostnames) | ||
| if skipped_unsafe > 0: | ||
| log.warning(f"Folder {sanitize_for_log(folder_name)}: skipped {skipped_unsafe} unsafe rules") |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (101/100) Warning
| @@ -0,0 +1,87 @@ | |||
| import pytest | |||
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Missing module docstring Warning test
| @@ -0,0 +1,87 @@ | |||
| import pytest | |||
| from unittest.mock import MagicMock | |||
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
standard import "from unittest.mock import MagicMock" should be placed before "import pytest" Warning test
| for call in calls: | ||
| args, kwargs = call | ||
| data = kwargs['data'] | ||
| for k, v in data.items(): |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "v" doesn't conform to snake_case naming style Warning test
| @@ -0,0 +1,87 @@ | |||
| import pytest | |||
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused import pytest Note test
| mock_post_form = MagicMock() | ||
|
|
||
| # Patch the API call | ||
| original_post_form = main._api_post_form |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _api_post_form of a client class Note test
|
|
||
| # Patch the API call | ||
| original_post_form = main._api_post_form | ||
| main._api_post_form = mock_post_form |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _api_post_form of a client class Note test
|
|
||
| sent_rules = [] | ||
| for call in calls: | ||
| args, kwargs = call |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused variable 'args' Note test
| assert found_unsafe_log | ||
|
|
||
| finally: | ||
| main._api_post_form = original_post_form |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Access to a protected member _api_post_form of a client class Note test
|
|
||
| for h in hostnames: | ||
| if not is_valid_rule(h): | ||
| log.warning(f"Skipping unsafe rule in {sanitize_for_log(folder_name)}: {sanitize_for_log(h)}") |
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (106/100) Warning
|
|
||
| duplicates_count = original_count - len(filtered_hostnames) | ||
| if skipped_unsafe > 0: | ||
| log.warning(f"Folder {sanitize_for_log(folder_name)}: skipped {skipped_unsafe} unsafe rules") |
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (101/100) Warning
| @@ -0,0 +1,87 @@ | |||
| import pytest | |||
Check warning
Code scanning / Pylint (reported by Codacy)
Missing module docstring Warning test
| @@ -0,0 +1,87 @@ | |||
| import pytest | |||
| from unittest.mock import MagicMock | |||
Check warning
Code scanning / Pylint (reported by Codacy)
standard import "from unittest.mock import MagicMock" should be placed before "import pytest" Warning test
| for call in calls: | ||
| args, kwargs = call | ||
| data = kwargs['data'] | ||
| for k, v in data.items(): |
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "v" doesn't conform to snake_case naming style Warning test
| @@ -0,0 +1,87 @@ | |||
| import pytest | |||
Check notice
Code scanning / Pylint (reported by Codacy)
Unused import pytest Note test
| mock_post_form = MagicMock() | ||
|
|
||
| # Patch the API call | ||
| original_post_form = main._api_post_form |
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _api_post_form of a client class Note test
|
|
||
| # Patch the API call | ||
| original_post_form = main._api_post_form | ||
| main._api_post_form = mock_post_form |
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _api_post_form of a client class Note test
|
|
||
| sent_rules = [] | ||
| for call in calls: | ||
| args, kwargs = call |
Check notice
Code scanning / Pylint (reported by Codacy)
Unused variable 'args' Note test
| assert found_unsafe_log | ||
|
|
||
| finally: | ||
| main._api_post_form = original_post_form |
Check notice
Code scanning / Pylint (reported by Codacy)
Access to a protected member _api_post_form of a client class Note test
There was a problem hiding this comment.
Pull request overview
This PR implements XSS validation for rule inputs from external blocklists by adding a new is_valid_rule() helper function and integrating it into the push_rules() function. The implementation filters out potentially malicious inputs (e.g., <script>, javascript:void(0)) from external blocklists before they can be stored in user profiles.
Changes:
- Added
is_valid_rule()validation function to block dangerous characters commonly used in XSS and injection attacks - Integrated validation into
push_rules()with logging for skipped unsafe rules - Added integration test in
tests/test_security.pyto verify XSS payloads are filtered - Documented the vulnerability and prevention strategy in
.jules/sentinel.md
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| main.py | Added is_valid_rule() function to validate rule safety and integrated it into push_rules() to filter out malicious inputs before API submission |
| tests/test_security.py | Added integration test to verify that XSS payloads are filtered while legitimate domain rules are allowed through |
| .jules/sentinel.md | Documented the indirect XSS vulnerability via third-party data and prevention measures |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def test_push_rules_filters_xss_payloads(): | ||
| """ | ||
| Verify that push_rules filters out malicious strings (XSS payloads). | ||
| """ | ||
| mock_client = MagicMock() | ||
| mock_post_form = MagicMock() | ||
|
|
||
| # Patch the API call | ||
| original_post_form = main._api_post_form | ||
| main._api_post_form = mock_post_form | ||
|
|
||
| # Patch the logger to verify warnings | ||
| mock_log = MagicMock() | ||
| original_log = main.log | ||
| main.log = mock_log | ||
|
|
||
| try: | ||
| malicious_rules = [ | ||
| "<script>alert(1)</script>", | ||
| "valid.com", | ||
| "javascript:void(0)", | ||
| "fail' OR '1'='1", | ||
| "img src=x onerror=alert(1)", | ||
| "safe-domain.com", | ||
| "1.1.1.1", | ||
| "*.wildcard.com" | ||
| ] | ||
|
|
||
| main.push_rules( | ||
| profile_id="p1", | ||
| folder_name="f1", | ||
| folder_id="fid1", | ||
| do=1, | ||
| status=1, | ||
| hostnames=malicious_rules, | ||
| existing_rules=set(), | ||
| client=mock_client | ||
| ) | ||
|
|
||
| # Check what was sent | ||
| assert mock_post_form.called | ||
| calls = mock_post_form.call_args_list | ||
|
|
||
| sent_rules = [] | ||
| for call in calls: | ||
| args, kwargs = call | ||
| data = kwargs['data'] | ||
| for k, v in data.items(): | ||
| if k.startswith("hostnames["): | ||
| sent_rules.append(v) | ||
|
|
||
| # EXPECTED BEHAVIOR: Malicious rules are NOT sent | ||
| assert "<script>alert(1)</script>" not in sent_rules | ||
| assert "javascript:void(0)" not in sent_rules # Contains parenthesis/colon? | ||
| # Wait, 'javascript:void(0)' has '('. My validator blocks '('. | ||
| assert "fail' OR '1'='1" not in sent_rules # Contains ' | ||
| assert "img src=x onerror=alert(1)" not in sent_rules # Contains ( ) or =? No = is allowed? | ||
| # "img src=x onerror=alert(1)" contains spaces? | ||
| # My validator: isprintable() is True. | ||
| # dangerous_chars: set("<>\"'`();{}[]") | ||
| # <script> has < > | ||
| # javascript:void(0) has ( ) | ||
| # fail' has ' | ||
| # img src=x ... has ( ) | ||
|
|
||
| # Valid rules MUST be sent | ||
| assert "valid.com" in sent_rules | ||
| assert "safe-domain.com" in sent_rules | ||
| assert "1.1.1.1" in sent_rules | ||
| assert "*.wildcard.com" in sent_rules | ||
|
|
||
| # Check logs for warnings | ||
| # We expect 4 skipped rules | ||
| assert mock_log.warning.call_count >= 1 | ||
| found_unsafe_log = False | ||
| for call in mock_log.warning.call_args_list: | ||
| if "Skipping unsafe rule" in str(call): | ||
| found_unsafe_log = True | ||
| assert found_unsafe_log | ||
|
|
||
| finally: | ||
| main._api_post_form = original_post_form | ||
| main.log = original_log |
There was a problem hiding this comment.
The test file uses a plain function with manual mocking rather than following the unittest.TestCase pattern used consistently in other test files (test_ssrf.py, test_ssrf_enhanced.py, test_ux.py). This inconsistency makes the test suite harder to maintain and run. Consider refactoring to use unittest.TestCase with proper setUp/tearDown methods for consistent test structure.
| import pytest | ||
| from unittest.mock import MagicMock | ||
| import main | ||
|
|
||
| def test_push_rules_filters_xss_payloads(): | ||
| """ | ||
| Verify that push_rules filters out malicious strings (XSS payloads). | ||
| """ | ||
| mock_client = MagicMock() | ||
| mock_post_form = MagicMock() | ||
|
|
||
| # Patch the API call | ||
| original_post_form = main._api_post_form | ||
| main._api_post_form = mock_post_form | ||
|
|
||
| # Patch the logger to verify warnings | ||
| mock_log = MagicMock() | ||
| original_log = main.log | ||
| main.log = mock_log | ||
|
|
||
| try: | ||
| malicious_rules = [ | ||
| "<script>alert(1)</script>", | ||
| "valid.com", | ||
| "javascript:void(0)", | ||
| "fail' OR '1'='1", | ||
| "img src=x onerror=alert(1)", | ||
| "safe-domain.com", | ||
| "1.1.1.1", | ||
| "*.wildcard.com" | ||
| ] | ||
|
|
||
| main.push_rules( | ||
| profile_id="p1", | ||
| folder_name="f1", | ||
| folder_id="fid1", | ||
| do=1, | ||
| status=1, | ||
| hostnames=malicious_rules, | ||
| existing_rules=set(), | ||
| client=mock_client | ||
| ) | ||
|
|
||
| # Check what was sent | ||
| assert mock_post_form.called | ||
| calls = mock_post_form.call_args_list | ||
|
|
||
| sent_rules = [] | ||
| for call in calls: | ||
| args, kwargs = call | ||
| data = kwargs['data'] | ||
| for k, v in data.items(): | ||
| if k.startswith("hostnames["): | ||
| sent_rules.append(v) | ||
|
|
||
| # EXPECTED BEHAVIOR: Malicious rules are NOT sent | ||
| assert "<script>alert(1)</script>" not in sent_rules | ||
| assert "javascript:void(0)" not in sent_rules # Contains parenthesis/colon? | ||
| # Wait, 'javascript:void(0)' has '('. My validator blocks '('. | ||
| assert "fail' OR '1'='1" not in sent_rules # Contains ' | ||
| assert "img src=x onerror=alert(1)" not in sent_rules # Contains ( ) or =? No = is allowed? | ||
| # "img src=x onerror=alert(1)" contains spaces? | ||
| # My validator: isprintable() is True. | ||
| # dangerous_chars: set("<>\"'`();{}[]") | ||
| # <script> has < > | ||
| # javascript:void(0) has ( ) | ||
| # fail' has ' | ||
| # img src=x ... has ( ) | ||
|
|
||
| # Valid rules MUST be sent | ||
| assert "valid.com" in sent_rules | ||
| assert "safe-domain.com" in sent_rules | ||
| assert "1.1.1.1" in sent_rules | ||
| assert "*.wildcard.com" in sent_rules | ||
|
|
||
| # Check logs for warnings | ||
| # We expect 4 skipped rules | ||
| assert mock_log.warning.call_count >= 1 | ||
| found_unsafe_log = False | ||
| for call in mock_log.warning.call_args_list: | ||
| if "Skipping unsafe rule" in str(call): | ||
| found_unsafe_log = True | ||
| assert found_unsafe_log | ||
|
|
||
| finally: | ||
| main._api_post_form = original_post_form | ||
| main.log = original_log |
There was a problem hiding this comment.
There is no direct unit test for the is_valid_rule function itself. While the integration test checks the behavior through push_rules, adding dedicated unit tests for is_valid_rule would improve test coverage and make it easier to verify edge cases like empty strings, non-printable characters, and each dangerous character individually.
…ng sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
Hi @google-lab-jules, please open a new pull request to apply any remaining changes based on the comments in this thread. Thanks! |
Acknowledged. I have implemented the security fix for validating rules to prevent XSS and have submitted the changes in the |
Validates rule content from external JSON lists to ensure they do not contain malicious characters (XSS payloads, control characters) before pushing to Control D API. Filters out unsafe rules and logs a warning. Fixes potential Stored XSS vulnerability via malicious blocklists.
Implemented
is_valid_rulehelper function and integrated it intopush_rulesto filter out potentially malicious inputs (e.g.<script>) from external blocklists.Added tests in
tests/test_security.py.PR created automatically by Jules for task 17576703938918876849 started by @abhimehro