diff options
Diffstat (limited to 'tests/test_rdap.py')
| -rw-r--r-- | tests/test_rdap.py | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/tests/test_rdap.py b/tests/test_rdap.py index 7f2f0d5..815c311 100644 --- a/tests/test_rdap.py +++ b/tests/test_rdap.py @@ -445,5 +445,94 @@ class Query(unittest.TestCase): self.assertEqual(len(calls), 5) +class QuotedQueryComponent(unittest.TestCase): + """The interpolated component cannot escape its path segment. + + contacts.is_queryable() is the admission point and validates every + candidate, but that guarantee has already leaked three times in this + module's history, each time the same shape: a validator applied to one + branch and forgotten on its sibling. These call query_ip and + query_domain DIRECTLY, bypassing contacts entirely, because a future + branch that skips worklist() is exactly the failure this layer exists + to survive. + """ + + IPV4 = {"services": [[["198.51.100.0/24"], ["https://rir.example.invalid/"]]]} + IPV6 = {"services": [[["fe80::/10"], ["https://rir.example.invalid/"]]]} + DNS = {"services": [[["invalid"], ["https://registry.example.invalid/"]]]} + + def _recorder(self): + calls = [] + + def fetch(url): + calls.append(url) + return {"handle": "X", "entities": []} + + return calls, fetch + + def test_a_traversal_in_an_ip_stays_under_the_ip_segment(self): + """server_for_ip refuses this string today, so the base is forced. + + Forcing it is the point: this asserts what query_ip does with a + component it was handed, not what today's lookup happens to reject. + """ + calls, fetch = self._recorder() + original = rdap.server_for_ip + rdap.server_for_ip = lambda address, data: "https://rir.example.invalid/" + try: + rdap.query_ip("198.51.100.7/../../etc", self.IPV4, fetch=fetch) + finally: + rdap.server_for_ip = original + + self.assertEqual( + calls, + ["https://rir.example.invalid/ip/198.51.100.7%2F..%2F..%2Fetc"], + ) + + def test_a_scope_id_in_an_ip_is_encoded_not_left_malformed(self): + """A bare % in a URL is a truncated escape, not a literal percent. + + server_for_ip accepts fe80::1%eth0 today, so this one reaches the + wire through the normal path. + """ + calls, fetch = self._recorder() + rdap.query_ip("fe80::1%eth0", self.IPV6, fetch=fetch) + + self.assertEqual( + calls, ["https://rir.example.invalid/ip/fe80%3A%3A1%25eth0"] + ) + + def test_a_query_and_fragment_in_a_host_are_not_live(self): + calls, fetch = self._recorder() + rdap.query_domain("victim?e=x#frag.example.invalid", self.DNS, fetch=fetch) + + self.assertEqual(len(calls), 1) + url = calls[0] + self.assertNotIn("?", url) + self.assertNotIn("#", url) + self.assertIn("%3F", url) + self.assertIn("%23", url) + + def test_a_normal_ip_url_is_unchanged(self): + calls, fetch = self._recorder() + rdap.query_ip("198.51.100.7", self.IPV4, fetch=fetch) + self.assertEqual(calls, ["https://rir.example.invalid/ip/198.51.100.7"]) + + def test_a_normal_host_url_is_unchanged(self): + calls, fetch = self._recorder() + rdap.query_domain("example.invalid", self.DNS, fetch=fetch) + self.assertEqual( + calls, ["https://registry.example.invalid/domain/example.invalid"] + ) + + def test_the_returned_candidate_is_unquoted(self): + """The manifest and the review dialog show the name, not the URL.""" + calls, fetch = self._recorder() + _, queried = rdap.query_domain( + "victim?e=x#frag.example.invalid", self.DNS, fetch=fetch + ) + self.assertEqual(queried, "victim?e=x#frag.example.invalid") + + if __name__ == "__main__": unittest.main() |
