Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,11 @@
**Prevention:**
1. Parse URLs and check hostnames against `localhost` and private IP ranges using `ipaddress` module.
2. Enforce strict length limits on user inputs (e.g., profile IDs) to prevent resource exhaustion or buffer abuse.

## 2025-02-23 - [DNS Rebinding / SSRF via Domain Resolution]

Check notice

Code scanning / Remark-lint (reported by Codacy)

Warn when references to undefined definitions are found. Note

[no-undefined-references] Found reference to undefined definition

Check notice

Code scanning / Remark-lint (reported by Codacy)

Warn when shortcut reference links are used. Note

[no-shortcut-reference-link] Use the trailing [] on reference links
**Vulnerability:** The `validate_folder_url` function checked for private IPs but only when the hostname was an IP literal. It failed to resolve domain names to check if they pointed to private IPs (DNS Rebinding risk).
**Learning:** Validating hostnames solely by string inspection is insufficient. A public-looking domain can resolve to `127.0.0.1`. Defense-in-depth requires resolving the hostname and validating the IP address before connection.
**Prevention:**
1. Perform DNS resolution (`socket.getaddrinfo`) on hostnames.
2. Check all resolved IPs against private/loopback ranges.
3. Fail safe if DNS resolution fails.
18 changes: 17 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import concurrent.futures
import threading
import ipaddress
import socket
from urllib.parse import urlparse
from typing import Dict, List, Optional, Any, Set, Sequence

Expand Down Expand Up @@ -210,7 +211,22 @@
return False
except ValueError:
# Not an IP literal, it's a domain.
pass
# Perform DNS resolution to check for private IPs (DNS Rebinding protection)
try:

Check notice

Code scanning / Pylint (reported by Codacy)

Catching too general exception Exception Note

Catching too general exception Exception
# Resolve hostname to check if it points to a private IP
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check warning

Code scanning / Prospector (reported by Codacy)

Unused variable 'family' (unused-variable) Warning

Unused variable 'family' (unused-variable)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'kind' Note

Unused variable 'kind'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'family' Note

Unused variable 'family'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'proto' Note

Unused variable 'proto'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'canonname' Note

Unused variable 'canonname'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'proto' Note

Unused variable 'proto'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'kind' Note

Unused variable 'kind'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'family' Note

Unused variable 'family'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'canonname' Note

Unused variable 'canonname'
ip_str = sockaddr[0]
resolved_ip = ipaddress.ip_address(ip_str)
if resolved_ip.is_private or resolved_ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain resolves to private IP {ip_str}): {sanitize_for_log(url)}")

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (125/100) Warning

Line too long (125/100)

Check warning

Code 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 warning

Code scanning / Pylint (reported by Codacy)

Line too long (125/100) Warning

Line too long (125/100)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions
return False
except (socket.gaierror, ValueError) as e:

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "e" doesn't conform to snake_case naming style Warning

Variable name "e" doesn't conform to snake_case naming style

Check warning

Code scanning / Prospector (reported by Codacy)

local variable 'e' is assigned to but never used (F841) Warning

local variable 'e' is assigned to but never used (F841)

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "e" doesn't conform to snake_case naming style Warning

Variable name "e" doesn't conform to snake_case naming style
log.warning(f"Skipping unsafe URL (DNS resolution failed): {sanitize_for_log(url)}")

Check warning

Code 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 notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions
return False
except Exception as e:

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "e" doesn't conform to snake_case naming style Warning

Variable name "e" doesn't conform to snake_case naming style
log.warning(f"Skipping unsafe URL (DNS check error): {e}")

Check warning

Code 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 notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions
return False

except Exception as e:
log.warning(f"Failed to validate URL {sanitize_for_log(url)}: {e}")
Expand Down Expand Up @@ -323,7 +339,7 @@
}
except (httpx.HTTPError, KeyError) as e:
log.error(f"Failed to list existing folders: {sanitize_for_log(e)}")
return {}

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "e" doesn't conform to snake_case naming style Warning

Variable name "e" doesn't conform to snake_case naming style

def get_all_existing_rules(client: httpx.Client, profile_id: str) -> Set[str]:
all_rules = set()
Expand Down Expand Up @@ -663,7 +679,7 @@
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_folder = {
executor.submit(
_process_single_folder,

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Catching too general exception Exception Note

Catching too general exception Exception
folder_data,
profile_id,
existing_rules,
Expand Down
Empty file added tests/__init__.py
Empty file.
35 changes: 35 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import unittest

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing module docstring Warning test

Missing module docstring

Check warning

Code scanning / Pylint (reported by Codacy)

Missing module docstring Warning test

Missing module docstring
from unittest.mock import patch
import socket
import logging
import sys
import os

# Add parent directory to path so we can import main
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

# Configure logging to avoid polluting output
logging.basicConfig(level=logging.CRITICAL)

import main

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Import "import main" should be placed at the top of the module Warning test

Import "import main" should be placed at the top of the module

Check warning

Code scanning / Prospector (reported by Codacy)

Reimport 'main' (imported line 9) (reimported) Warning test

Reimport 'main' (imported line 9) (reimported)

Check warning

Code scanning / Pylint (reported by Codacy)

Import "import main" should be placed at the top of the module Warning test

Import "import main" should be placed at the top of the module

class TestSecurity(unittest.TestCase):

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing class docstring Warning test

Missing class docstring

Check warning

Code scanning / Pylint (reported by Codacy)

Missing class docstring Warning test

Missing class docstring
def test_validate_folder_url_prevents_dns_rebinding(self):
"""
Verify that the implementation prevents domains that resolve to private IPs.
"""
suspicious_url = "https://internal.example.com/list.json"

# Mock socket.getaddrinfo to return 127.0.0.1
with patch("socket.getaddrinfo") as mock_dns:
mock_dns.return_value = [
(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('127.0.0.1', 443))
]

result = main.validate_folder_url(suspicious_url)

# Should be False (Secure)
self.assertFalse(result, "validate_folder_url should return False for domains resolving to private IPs")

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (116/100) Warning test

Line too long (116/100)

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (116/100) Warning test

Line too long (116/100)

if __name__ == "__main__":

Check warning

Code scanning / Prospector (reported by Codacy)

expected 2 blank lines after class or function definition, found 1 (E305) Warning test

expected 2 blank lines after class or function definition, found 1 (E305)
unittest.main()
Loading