Coverage for src/local_deep_research/security/legacy_ipv4.py: 96%

64 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-06 15:42 +0000

1"""Recognize ambiguous historical IPv4 host syntax without network access.""" 

2 

3import ipaddress 

4from typing import Final 

5from urllib.parse import unquote_to_bytes 

6 

7 

8AMBIGUOUS_NUMERIC_IPV4_HOST_ERROR: Final = "Ambiguous numeric IPv4 host" 

9ENCODED_NUMERIC_IPV4_HOST_ERROR: Final = "Encoded numeric IPv4 host" 

10_COMPONENT_MAXIMUMS: Final[tuple[tuple[int, ...], ...]] = ( 

11 (0xFFFFFFFF,), 

12 (0xFF, 0xFFFFFF), 

13 (0xFF, 0xFF, 0xFFFF), 

14 (0xFF, 0xFF, 0xFF, 0xFF), 

15) 

16 

17 

18def is_ambiguous_numeric_ipv4_host(hostname: str) -> bool: 

19 """Return whether ``hostname`` is a valid noncanonical historical IPv4 form. 

20 

21 This recognizes the one-to-four-part inet_aton-style grammar using decimal, 

22 leading-zero octal, and ``0x`` hexadecimal components. It deliberately does 

23 not perform DNS or platform parsing. 

24 """ 

25 try: 

26 _ = ipaddress.ip_address(hostname) 

27 except ValueError: 

28 components = hostname.split(".") 

29 else: 

30 return False 

31 

32 if not 1 <= len(components) <= len(_COMPONENT_MAXIMUMS): 

33 return False 

34 

35 maximums = _COMPONENT_MAXIMUMS[len(components) - 1] 

36 return all( 

37 _parse_component(component, maximum) is not None 

38 for component, maximum in zip(components, maximums, strict=True) 

39 ) 

40 

41 

42def is_percent_encoded_numeric_ipv4_host(hostname: str) -> bool: 

43 """Return whether one-pass percent decoding exposes an IPv4 address form.""" 

44 decoded_hostname = _decode_percent_escapes(hostname) 

45 if decoded_hostname is None or decoded_hostname == hostname: 

46 return False 

47 classification_hostname = decoded_hostname.rstrip(".") 

48 

49 try: 

50 parsed_ip = ipaddress.ip_address(classification_hostname) 

51 except ValueError: 

52 return is_ambiguous_numeric_ipv4_host(classification_hostname) 

53 return isinstance(parsed_ip, ipaddress.IPv4Address) 

54 

55 

56def _decode_percent_escapes(hostname: str) -> str | None: 

57 """Decode only well-formed ASCII percent escapes without recursive decoding.""" 

58 position = 0 

59 while (percent := hostname.find("%", position)) >= 0: 

60 if ( 60 ↛ 65line 60 didn't jump to line 65 because the condition on line 60 was never true

61 percent + 2 >= len(hostname) 

62 or _digit_value(hostname[percent + 1]) is None 

63 or _digit_value(hostname[percent + 2]) is None 

64 ): 

65 return None 

66 position = percent + 3 

67 

68 try: 

69 return unquote_to_bytes(hostname).decode("ascii") 

70 except UnicodeDecodeError: 

71 return None 

72 

73 

74def _parse_component(component: str, maximum: int) -> int | None: 

75 """Parse one historical IPv4 component without integer overflow.""" 

76 if not component: 

77 return None 

78 

79 base = 10 

80 digits = component 

81 if component.startswith(("0x", "0X")): 

82 base = 16 

83 digits = component[2:] 

84 elif len(component) > 1 and component.startswith("0"): 

85 base = 8 

86 digits = component[1:] 

87 

88 if not digits: 

89 return None 

90 

91 value = 0 

92 for character in digits: 

93 digit = _digit_value(character) 

94 if digit is None or digit >= base or value > (maximum - digit) // base: 

95 return None 

96 value = value * base + digit 

97 return value 

98 

99 

100def _digit_value(character: str) -> int | None: 

101 if "0" <= character <= "9": 

102 return ord(character) - ord("0") 

103 if "a" <= character <= "f": 

104 return ord(character) - ord("a") + 10 

105 if "A" <= character <= "F": 

106 return ord(character) - ord("A") + 10 

107 return None