-
Notifications
You must be signed in to change notification settings - Fork 1
🛡️ Sentinel: [SECURITY] Enforce secure URL validation and file permissions #169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Sentinel's Journal | ||
|
|
||
| ## 2025-02-07 - Secure File Creation Pattern | ||
| **Vulnerability:** Default `open()` creates files with user umask, which might allow group/world read access for sensitive files like `plan.json`. | ||
| **Learning:** Python's `open()` mode `w` respects `umask` but doesn't enforce restrictive permissions by default. `os.chmod` after creation leaves a small race condition window. | ||
| **Prevention:** Use `os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)` to create the file descriptor with correct permissions atomically, then wrap with `os.fdopen()`. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -340,6 +340,12 @@ | |||||||||||||||||||||||||||
| if not hostname: | ||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if parsed.username or parsed.password: | ||||||||||||||||||||||||||||
| log.warning( | ||||||||||||||||||||||||||||
| f"Skipping unsafe URL (credentials detected): {sanitize_for_log(url)}" | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| f"Skipping unsafe URL (credentials detected): {sanitize_for_log(url)}" | |
| f"Skipping unsafe URL (credentials detected for host {hostname})" |
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "fd" doesn't conform to snake_case naming style Warning
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "fd" doesn't conform to snake_case naming style Warning
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating the file with mode 0o600 only guarantees restrictive permissions when the file is newly created; if args.plan_json already exists with broader permissions, os.open(...|O_TRUNC, 0o600) will not tighten them. Consider applying os.fchmod(fd, 0o600) immediately after opening, or writing to a secure temp file (0600) and os.replace it into place to ensure the final file is always private.
| fd = os.open(args.plan_json, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) | |
| fd = os.open(args.plan_json, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) | |
| try: | |
| # Ensure restrictive permissions even if file already existed | |
| os.fchmod(fd, 0o600) | |
| except OSError: | |
| # If we cannot adjust permissions, proceed as before | |
| pass |
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "f" doesn't conform to snake_case naming style Warning
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "f" doesn't conform to snake_case naming style Warning
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning
Outdated
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Outdated
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This except OSError logs the failure to write --plan-json but then continues; the process can still exit 0 even though the user explicitly requested an output file. Consider failing the run (e.g., exit(1)/re-raise) when --plan-json is provided but cannot be written.
| log.error(f"Failed to write plan to {args.plan_json}: {e}") | |
| log.error(f"Failed to write plan to {args.plan_json}: {e}") | |
| sys.exit(1) |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error log uses an f-string with the raw args.plan_json path. Since this codebase uses sanitize_for_log to escape control characters and prevent log injection, consider logging sanitize_for_log(args.plan_json) (and sanitize_for_log(e)) and/or using logger formatting (log.error("... %s", ...)) to avoid embedding untrusted strings directly.
| log.info("Plan written to %s", args.plan_json) | |
| except OSError as e: | |
| log.error(f"Failed to write plan to {args.plan_json}: {e}") | |
| log.info("Plan written to %s", sanitize_for_log(args.plan_json)) | |
| except OSError as e: | |
| log.error( | |
| "Failed to write plan to %s: %s", | |
| sanitize_for_log(args.plan_json), | |
| sanitize_for_log(e), | |
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||||||
| import importlib | ||||||||||||||||||||||
| import os | ||||||||||||||||||||||
| import stat | ||||||||||||||||||||||
| import sys | ||||||||||||||||||||||
| from unittest.mock import MagicMock, call, patch | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -74,7 +75,7 @@ | |||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Case 3: push_rules updates data dictionary with pre-calculated batch keys correctly | ||||||||||||||||||||||
| def test_push_rules_updates_data_with_batch_keys(monkeypatch): | ||||||||||||||||||||||
Check warningCode scanning / Pylint (reported by Codacy) Variable name "m" doesn't conform to snake_case naming style Warning test
Variable name "m" doesn't conform to snake_case naming style
|
||||||||||||||||||||||
| m = reload_main_with_env(monkeypatch) | ||||||||||||||||||||||
| mock_client = MagicMock() | ||||||||||||||||||||||
| mock_post_form = MagicMock() | ||||||||||||||||||||||
|
|
@@ -257,7 +258,7 @@ | |||||||||||||||||||||
| assert m.check_api_access(mock_client, "valid_profile") is True | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def test_check_api_access_401(monkeypatch): | ||||||||||||||||||||||
Check warningCode scanning / Pylintpython3 (reported by Codacy) Variable name "m" doesn't conform to snake_case naming style Warning test
Variable name "m" doesn't conform to snake_case naming style
|
||||||||||||||||||||||
| m = reload_main_with_env(monkeypatch) | ||||||||||||||||||||||
| mock_client = MagicMock() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -320,7 +321,7 @@ | |||||||||||||||||||||
| assert "Profile Not Found" in str(mock_log.critical.call_args_list) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def test_check_api_access_generic_http_error(monkeypatch): | ||||||||||||||||||||||
Check warningCode scanning / Pylint (reported by Codacy) Variable name "m" doesn't conform to snake_case naming style Warning test
Variable name "m" doesn't conform to snake_case naming style
|
||||||||||||||||||||||
| m = reload_main_with_env(monkeypatch) | ||||||||||||||||||||||
| mock_client = MagicMock() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -510,3 +511,80 @@ | |||||||||||||||||||||
| # Color codes (accessing instance Colors or m.Colors) | ||||||||||||||||||||||
| assert m.Colors.CYAN in combined | ||||||||||||||||||||||
| assert m.Colors.ENDC in combined | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Case 14: validate_folder_url rejects URLs with credentials | ||||||||||||||||||||||
| def test_validate_folder_url_rejects_credentials(monkeypatch): | ||||||||||||||||||||||
Check warningCode scanning / Pylint (reported by Codacy) Missing function docstring Warning test
Missing function docstring
Check warningCode scanning / Pylintpython3 (reported by Codacy) Missing function or method docstring Warning test
Missing function or method docstring
|
||||||||||||||||||||||
| m = reload_main_with_env(monkeypatch) | ||||||||||||||||||||||
Check warningCode scanning / Pylintpython3 (reported by Codacy) Variable name "m" doesn't conform to snake_case naming style Warning test
Variable name "m" doesn't conform to snake_case naming style
|
||||||||||||||||||||||
| mock_log = MagicMock() | ||||||||||||||||||||||
| monkeypatch.setattr(m, "log", mock_log) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Use a mock for socket.getaddrinfo so we don't hit network or fail DNS | ||||||||||||||||||||||
| # But validate_folder_url logic for credentials happens BEFORE DNS. | ||||||||||||||||||||||
| # So we don't strictly need to mock DNS if it returns early. | ||||||||||||||||||||||
| # However, if it falls through (fails logic), it hits DNS. | ||||||||||||||||||||||
| # To be safe and isolated, mock getaddrinfo. | ||||||||||||||||||||||
| with patch("socket.getaddrinfo") as mock_dns: | ||||||||||||||||||||||
| mock_dns.return_value = [(2, 1, 6, "", ("1.1.1.1", 443))] | ||||||||||||||||||||||
| # URL with credentials | ||||||||||||||||||||||
| url = "https://user:pass@example.com/list.json" | ||||||||||||||||||||||
| assert m.validate_folder_url(url) is False | ||||||||||||||||||||||
Check noticeCode scanning / Bandit Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Check noticeCode scanning / Bandit (reported by Codacy) Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
|
||||||||||||||||||||||
| assert mock_log.warning.called | ||||||||||||||||||||||
Check noticeCode scanning / Bandit Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Check noticeCode scanning / Bandit (reported by Codacy) Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
|
||||||||||||||||||||||
| # Check that warning message contains "credentials detected" | ||||||||||||||||||||||
| # call_args is (args, kwargs) | ||||||||||||||||||||||
| args, _ = mock_log.warning.call_args | ||||||||||||||||||||||
| assert "credentials detected" in args[0] | ||||||||||||||||||||||
Check noticeCode scanning / Bandit Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Check noticeCode scanning / Bandit (reported by Codacy) Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
|
||||||||||||||||||||||
| assert "credentials detected" in args[0] | |
| assert "credentials detected" in args[0] | |
| # Ensure that raw credentials are not leaked in the warning message | |
| assert "user:pass" not in args[0] |
Check warning
Code scanning / Prospector (reported by Codacy)
Unused argument 'capsys' (unused-argument) Warning test
Check warning
Code scanning / Pylint (reported by Codacy)
Missing function docstring Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Unused argument 'capsys' Note test
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Missing function or method docstring Warning test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused argument 'capsys' Note test
Check notice
Code scanning / Bandit
Possible hardcoded password: 'dummy' Note test
Check warning
Code scanning / Prospector (reported by Codacy)
Unused argument 'no_delete' (unused-argument) Warning test
Check warning
Code scanning / Prospector (reported by Codacy)
Unused argument 'urls' (unused-argument) Warning test
Check warning
Code scanning / Prospector (reported by Codacy)
Unused argument 'dry_run' (unused-argument) Warning test
Check warning
Code scanning / Pylint (reported by Codacy)
Missing function docstring Warning test
Check notice
Code scanning / Pylint (reported by Codacy)
Unused argument 'urls' Note test
Check notice
Code scanning / Pylint (reported by Codacy)
Unused argument 'dry_run' Note test
Check notice
Code scanning / Pylint (reported by Codacy)
Unused argument 'no_delete' Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused argument 'no_delete' Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused argument 'urls' Note test
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused argument 'dry_run' Note test
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning test
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning test
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Outdated
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 notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Outdated
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 notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Outdated
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 notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Outdated
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 notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Outdated
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 notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Outdated
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current permission check is good, but it could be more robust. It verifies that owner-read and owner-write permissions are set, and group/other permissions are not, but it doesn't ensure that other permission bits (like owner-execute) are unset. A more precise approach is to check that the file mode is exactly 0o600.
| mode = os.stat(plan_file).st_mode | |
| # Check permissions are 600 (rw-------) | |
| # S_IRWXG = 0o070 (group rwx) | |
| # S_IRWXO = 0o007 (other rwx) | |
| assert not (mode & stat.S_IRWXG), "Group should have no permissions" | |
| assert not (mode & stat.S_IRWXO), "Others should have no permissions" | |
| assert mode & stat.S_IRUSR, "Owner should have read" | |
| assert mode & stat.S_IWUSR, "Owner should have write" | |
| permissions = stat.S_IMODE(os.stat(plan_file).st_mode) | |
| assert permissions == 0o600, f"Expected file permissions to be 0o600, but got {oct(permissions)}" |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note