-
Notifications
You must be signed in to change notification settings - Fork 1
π‘οΈ Sentinel: [Security Enhancement] Validate Resource IDs & Harden Folder Names #154
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,4 @@ | ||
| ## 2025-10-26 - Trusting API IDs | ||
| **Vulnerability:** IDOR / Path Traversal risk if API returns unsafe IDs. | ||
| **Learning:** Even "trusted" upstream APIs can be compromised or buggy. Trust nothing. | ||
| **Prevention:** Validate all resource IDs (PKs) from APIs against a strict whitelist before using them in local operations or downstream requests. |
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -397,25 +397,34 @@ | |||||||||||||||||||||||
| return text | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def is_valid_profile_id_format(profile_id: str) -> bool: | ||||||||||||||||||||||||
| if not re.match(r"^[a-zA-Z0-9_-]+$", profile_id): | ||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||
| if len(profile_id) > 64: | ||||||||||||||||||||||||
| def validate_resource_id( | ||||||||||||||||||||||||
| resource_id: str, type_name: str = "ID", log_errors: bool = True | ||||||||||||||||||||||||
| ) -> bool: | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| Validates a resource ID (Profile ID, Folder ID/PK). | ||||||||||||||||||||||||
| Allowed: Alphanumeric, hyphen, underscore. | ||||||||||||||||||||||||
| Max Length: 64 | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| if not resource_id: | ||||||||||||||||||||||||
Check warningCode scanning / Pylint (reported by Codacy) Missing function docstring Warning
Missing function docstring
|
||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if len(resource_id) > 64: | ||||||||||||||||||||||||
| if log_errors: | ||||||||||||||||||||||||
| log.error(f"Invalid {type_name} length (max 64 chars)") | ||||||||||||||||||||||||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def validate_profile_id(profile_id: str, log_errors: bool = True) -> bool: | ||||||||||||||||||||||||
| if not is_valid_profile_id_format(profile_id): | ||||||||||||||||||||||||
| if not re.match(r"^[a-zA-Z0-9_-]+$", resource_id): | ||||||||||||||||||||||||
| if log_errors: | ||||||||||||||||||||||||
| if not re.match(r"^[a-zA-Z0-9_-]+$", profile_id): | ||||||||||||||||||||||||
| log.error("Invalid profile ID format (contains unsafe characters)") | ||||||||||||||||||||||||
| elif len(profile_id) > 64: | ||||||||||||||||||||||||
| log.error("Invalid profile ID length (max 64 chars)") | ||||||||||||||||||||||||
| log.error(f"Invalid {type_name} format (contains unsafe characters)") | ||||||||||||||||||||||||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||
|
Comment on lines
+416
to
419
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regular expression For example: _RESOURCE_ID_RE = re.compile(r'^[a-zA-Z0-9_-]+$')Then use |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def validate_profile_id(profile_id: str, log_errors: bool = True) -> bool: | ||||||||||||||||||||||||
| return validate_resource_id(profile_id, "profile ID", log_errors) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def is_valid_rule(rule: str) -> bool: | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| Validates that a rule is safe to use. | ||||||||||||||||||||||||
|
|
@@ -436,15 +445,17 @@ | |||||||||||||||||||||||
| def is_valid_folder_name(name: str) -> bool: | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| Validates folder name to prevent XSS and ensure printability. | ||||||||||||||||||||||||
| Allowed: Anything printable except < > " ' ` | ||||||||||||||||||||||||
| Allowed: Alphanumeric, space, hyphen, dot, underscore, parens, brackets, braces. | ||||||||||||||||||||||||
| Max Length: 64 | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| if not name or not name.strip() or not name.isprintable(): | ||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Block XSS and HTML injection characters | ||||||||||||||||||||||||
| # Allow: ( ) [ ] { } for folder names (e.g. "Work (Private)") | ||||||||||||||||||||||||
| dangerous_chars = set("<>\"'`") | ||||||||||||||||||||||||
| if any(c in dangerous_chars for c in name): | ||||||||||||||||||||||||
| if len(name) > 64: | ||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Use lazy % formatting in logging functions Note
Use lazy % formatting in logging functions
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Whitelist: Alphanumeric, space, hyphen, dot, underscore, parens, brackets, braces | ||||||||||||||||||||||||
| if not re.match(r"^[a-zA-Z0-9 \-_.()\[\]{}]+$", name): | ||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||
|
Comment on lines
+458
to
459
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||
|
|
@@ -461,7 +472,7 @@ | |||||||||||||||||||||||
| return False | ||||||||||||||||||||||||
| if not isinstance(data["group"], dict): | ||||||||||||||||||||||||
| log.error( | ||||||||||||||||||||||||
| f"Invalid data from {sanitize_for_log(url)}: 'group' must be an object." | ||||||||||||||||||||||||
Check warningCode scanning / Prospector (reported by Codacy) Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Use lazy % formatting in logging functions (logging-fstring-interpolation)
|
||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||
| if "group" not in data["group"]: | ||||||||||||||||||||||||
|
|
@@ -627,11 +638,18 @@ | |||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| data = _api_get(client, f"{API_BASE}/{profile_id}/groups").json() | ||||||||||||||||||||||||
| folders = data.get("body", {}).get("groups", []) | ||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| f["group"].strip(): f["PK"] | ||||||||||||||||||||||||
| for f in folders | ||||||||||||||||||||||||
| if f.get("group") and f.get("PK") | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| result = {} | ||||||||||||||||||||||||
| for f in folders: | ||||||||||||||||||||||||
Check warningCode scanning / Pylint (reported by Codacy) Variable name "f" doesn't conform to snake_case naming style Warning
Variable name "f" doesn't conform to snake_case naming style
Check warningCode scanning / Pylintpython3 (reported by Codacy) Variable name "f" doesn't conform to snake_case naming style Warning
Variable name "f" doesn't conform to snake_case naming style
|
||||||||||||||||||||||||
| name = f.get("group") | ||||||||||||||||||||||||
| pk = f.get("PK") | ||||||||||||||||||||||||
Check warningCode scanning / Pylint (reported by Codacy) Variable name "pk" doesn't conform to snake_case naming style Warning
Variable name "pk" doesn't conform to snake_case naming style
Check warningCode scanning / Pylintpython3 (reported by Codacy) Variable name "pk" doesn't conform to snake_case naming style Warning
Variable name "pk" doesn't conform to snake_case naming style
|
||||||||||||||||||||||||
| if name and pk: | ||||||||||||||||||||||||
| if validate_resource_id(pk, "Folder PK", log_errors=False): | ||||||||||||||||||||||||
| result[name.strip()] = pk | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| log.warning( | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| f"Skipping folder '{sanitize_for_log(name)}' with unsafe PK: {sanitize_for_log(pk)}" | ||||||||||||||||||||||||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (108/100) Warning
Line too long (108/100)
Check warningCode scanning / Pylintpython3 (reported by Codacy) Line too long (108/100) Warning
Line too long (108/100)
Comment on lines
+646
to
+650
|
||||||||||||||||||||||||
| if validate_resource_id(pk, "Folder PK", log_errors=False): | |
| result[name.strip()] = pk | |
| else: | |
| log.warning( | |
| f"Skipping folder '{sanitize_for_log(name)}' with unsafe PK: {sanitize_for_log(pk)}" | |
| pk_str = str(pk) | |
| if validate_resource_id(pk_str, "Folder PK", log_errors=False): | |
| result[name.strip()] = pk_str | |
| else: | |
| log.warning( | |
| f"Skipping folder '{sanitize_for_log(name)}' with unsafe PK: {sanitize_for_log(pk_str)}" |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
Check warning
Code scanning / Pylint (reported by Codacy)
Unnecessary "else" after "return" Warning
Check warning
Code scanning / Prospector (reported by Codacy)
Unnecessary "else" after "return" (no-else-return) Warning
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Unnecessary "else" after "return" Warning
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (109/100) Warning
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (109/100) Warning
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 block of code for validating a new folder's PK, logging, and returning is duplicated in this function (here, in the next if block, and in the polling fallback). To improve maintainability and reduce redundancy, consider extracting this logic into a private helper function.
Here's an example of what that helper could look like:
def _validate_and_return_pk(pk: Any, name: str, context: str) -> Optional[str]:
pk_str = str(pk)
if validate_resource_id(pk_str, f"{context} Folder PK"):
log.info(
"Created folder %s (ID %s) [%s]", sanitize_for_log(name), pk, context
)
return pk_str
else:
log.error(
f"API returned unsafe PK for folder {sanitize_for_log(name)}: {sanitize_for_log(pk)}"
)
return None
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "pk" doesn't conform to snake_case naming style Warning
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "pk" doesn't conform to snake_case naming style Warning
Outdated
Check warning
Code scanning / Prospector (reported by Codacy)
Unnecessary "else" after "return" (no-else-return) Warning
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Unnecessary "else" after "return" Warning
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Unnecessary "else" after "return" Warning
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (117/100) Warning
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (117/100) Warning
Outdated
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "pk" doesn't conform to snake_case naming style Warning
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "pk" doesn't conform to snake_case naming style Warning
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Unnecessary "else" after "return" Warning
Outdated
Check warning
Code scanning / Prospector (reported by Codacy)
Unnecessary "else" after "return" (no-else-return) Warning
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Unnecessary "else" after "return" Warning
Dismissed
Show dismissed
Hide dismissed
Outdated
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (117/100) Warning
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (117/100) Warning
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import pytest | ||
Check warningCode scanning / Pylintpython3 (reported by Codacy) Missing module docstring Warning test
Missing module docstring
Check warningCode scanning / Prospector (reported by Codacy) Unable to import 'pytest' (import-error) Warning test
Unable to import 'pytest' (import-error)
Check warningCode scanning / Prospector (reported by Codacy) Unused import pytest (unused-import) Warning test
Unused import pytest (unused-import)
Check warningCode scanning / Pylint (reported by Codacy) Missing module docstring Warning test
Missing module docstring
Check noticeCode scanning / Pylint (reported by Codacy) Unused import pytest Note test
Unused import pytest
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Unused import pytest Note test
Unused import pytest
|
||
| from unittest.mock import MagicMock | ||
Check warningCode scanning / Pylintpython3 (reported by Codacy) standard import "from unittest.mock import MagicMock" should be placed before "import pytest" Warning test
standard import "from unittest.mock import MagicMock" should be placed before "import pytest"
Check warningCode scanning / Pylint (reported by Codacy) standard import "from unittest.mock import MagicMock" should be placed before "import pytest" Warning test
standard import "from unittest.mock import MagicMock" should be placed before "import pytest"
|
||
| import main | ||
|
Comment on lines
+1
to
+3
|
||
|
|
||
| def test_validate_resource_id_valid(): | ||
| """Test valid resource IDs.""" | ||
| valid_ids = [ | ||
| "123", | ||
| "abc", | ||
| "ABC", | ||
| "Folder_1", | ||
| "Profile-2", | ||
| "valid_id_123-ABC", | ||
| "a" * 64, # Max length | ||
| ] | ||
| for rid in valid_ids: | ||
| assert main.validate_resource_id(rid) is True, f"Should accept {rid}" | ||
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.
|
||
|
|
||
| def test_validate_resource_id_invalid_format(): | ||
| """Test invalid formats (unsafe chars).""" | ||
| invalid_ids = [ | ||
| "id with space", | ||
| "id/slash", | ||
| "id\\backslash", | ||
| "id.dot", | ||
| "id:colon", | ||
| "id<script>", | ||
| "../../etc/passwd", | ||
| "; drop table", | ||
| "&", | ||
| "$", | ||
| ] | ||
| # Mock log to silence errors during test | ||
| original_log = main.log | ||
| main.log = MagicMock() | ||
| try: | ||
| for rid in invalid_ids: | ||
| assert main.validate_resource_id(rid) is False, f"Should reject {rid}" | ||
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.
|
||
| finally: | ||
| main.log = original_log | ||
|
Comment on lines
+33
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The For example: @pytest.fixture
def mock_log(monkeypatch):
"""Fixture to mock the main logger."""
mock = MagicMock()
monkeypatch.setattr(main, "log", mock)
return mock
def test_validate_resource_id_invalid_format(mock_log):
"""Test invalid formats (unsafe chars)."""
invalid_ids = [
# ...
]
for rid in invalid_ids:
assert main.validate_resource_id(rid) is False, f"Should reject {rid}" |
||
|
|
||
| def test_validate_resource_id_invalid_length(): | ||
| """Test invalid length.""" | ||
| original_log = main.log | ||
| main.log = MagicMock() | ||
| try: | ||
| long_id = "a" * 65 | ||
| assert main.validate_resource_id(long_id) 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.
|
||
| finally: | ||
| main.log = original_log | ||
|
|
||
| def test_validate_resource_id_empty(): | ||
| """Test empty ID.""" | ||
| assert main.validate_resource_id("") 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 main.validate_resource_id(None) 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.
|
||
|
|
||
| def test_is_valid_folder_name_whitelist(): | ||
| """Test the stricter whitelist for folder names.""" | ||
| valid_names = [ | ||
| "Work", | ||
| "Home Network", | ||
| "Folder (Private)", | ||
| "Use [Brackets]", | ||
| "Use {Braces}", | ||
| "Hyphen-ated", | ||
| "Under_score", | ||
| "Dot.Name", | ||
| "123456", | ||
| ] | ||
| for name in valid_names: | ||
| assert main.is_valid_folder_name(name) is True, f"Should accept {name}" | ||
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.
|
||
|
|
||
| invalid_names = [ | ||
| "Folder with <", | ||
| "Folder with >", | ||
| "Folder with '", | ||
| "Folder with \"", | ||
| "Folder with `", | ||
| "Folder with ;", # New restriction | ||
| "Folder with &", # New restriction | ||
| "Folder with |", # New restriction | ||
| "Folder with $", # New restriction | ||
| "Folder with \\", # New restriction | ||
| ] | ||
| for name in invalid_names: | ||
| assert main.is_valid_folder_name(name) is False, f"Should reject {name}" | ||
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.
|
||
|
|
||
| def test_is_valid_folder_name_length(): | ||
| """Test folder name length limit.""" | ||
| long_name = "a" * 65 | ||
| assert main.is_valid_folder_name(long_name) 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.
|
||
Check warning
Code scanning / Pylint (reported by Codacy)
Wrong hanging indentation before block (add 4 spaces). Warning