Sync reading position from Moon Reader app to Bookhive atproto records
atproto bookhive ereader moonreader
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 95 lines 3.5 kB view raw
1"""Guard-rail unit tests added during the pre-publish security review. 2 3Each test pins down one specific hardening from the review: 4 1. PROPFIND displayname escapes XML-special chars 5 2. Passthrough refuses sibling-prefix directory escapes 6 3. upload_cover rejects non-https / private-IP / localhost URLs 7""" 8 9from __future__ import annotations 10 11import pytest 12 13from spacebee.adapters.webdav import moonreader, passthrough 14from spacebee.atproto.bookhive import _is_safe_cover_url 15 16# 1. XML escaping in PROPFIND response ------------------------------------- 17 18def test_propfind_displayname_escapes_special_chars(): 19 xml = moonreader._response_xml( 20 href="/Books/.Moon+/Cache/Rosencrantz & Guildenstern.epub.po", 21 is_collection=False, 22 last_modified_http="Thu, 01 Jan 1970 00:00:00 GMT", 23 content_length=0, 24 etag='"abc"', 25 display_name="Rosencrantz & Guildenstern <script>.epub.po", 26 ) 27 assert "&amp;" in xml 28 assert "&lt;script&gt;" in xml 29 # The raw '<' must not appear as the start of a tag we didn't write. 30 assert "<script>" not in xml 31 32 33def test_passthrough_entry_xml_escapes_local_name(tmp_path): 34 sneaky = tmp_path / "R&D <plan>.txt" 35 sneaky.write_text("hello") 36 xml = passthrough._entry_xml(sneaky, "/R&D <plan>.txt", tmp_path) 37 assert "&amp;" in xml 38 assert "&lt;plan&gt;" in xml 39 assert "<plan>" not in xml 40 41 42# 2. Path-traversal guard -------------------------------------------------- 43 44def test_passthrough_refuses_parent_escape(tmp_path): 45 root = tmp_path / "pass" 46 root.mkdir() 47 p = passthrough.Passthrough(str(root)) 48 with pytest.raises(PermissionError): 49 p._local("/../etc/passwd") 50 51 52def test_passthrough_refuses_sibling_prefix_escape(tmp_path): 53 # Regression for the str.startswith antipattern — a sibling like 54 # `/tmp/.../pass_evil` must not pass when root is `/tmp/.../pass`. 55 root = tmp_path / "pass" 56 root.mkdir() 57 (tmp_path / "passerby").mkdir() 58 p = passthrough.Passthrough(str(root)) 59 with pytest.raises(PermissionError): 60 p._local("/../passerby/secret") 61 62 63# 3. Cover-URL SSRF guard -------------------------------------------------- 64 65@pytest.mark.parametrize( 66 "url", 67 [ 68 "https://covers.bookhive.buzz/img/abc.jpg", 69 "https://images.example.com/covers/12345", 70 ], 71) 72def test_safe_cover_urls_accepted(url): 73 assert _is_safe_cover_url(url) 74 75 76@pytest.mark.parametrize( 77 "url", 78 [ 79 "http://covers.bookhive.buzz/img/abc.jpg", # plaintext 80 "https://localhost/foo", # localhost hostname 81 "https://localhost.localdomain/x", # localhost alias 82 "https://foo.localhost/x", # .localhost suffix 83 "https://127.0.0.1/x", # loopback IPv4 84 "https://[::1]/x", # loopback IPv6 85 "https://169.254.169.254/latest/meta-data/", # AWS IMDS 86 "https://10.0.0.5/x", # RFC1918 87 "https://192.168.1.1/x", # RFC1918 88 "file:///etc/passwd", # not http(s) 89 "gopher://example.com/", # not http(s) 90 "", # empty 91 "not a url at all", # malformed 92 ], 93) 94def test_unsafe_cover_urls_rejected(url): 95 assert not _is_safe_cover_url(url)