diff --git a/archivebox/core/admin_archiveresults.py b/archivebox/core/admin_archiveresults.py index a1436942..db569ceb 100644 --- a/archivebox/core/admin_archiveresults.py +++ b/archivebox/core/admin_archiveresults.py @@ -142,18 +142,22 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None): process = result.process_record process_display = "-" if process: + process_url = html.escape(reverse("admin:machine_process_change", args=[process.id]), quote=True) + process_label = html.escape(get_process_link_label(process), quote=True) process_display = f''' - {get_process_link_label(process)} + title="View process">{process_label} ''' machine_display = "-" if process and process.machine_id: + machine_url = html.escape(reverse("admin:machine_machine_change", args=[process.machine_id]), quote=True) + machine_label = html.escape(str(process.machine.hostname or ""), quote=True) machine_display = f''' - {process.machine.hostname} + title="View machine">{machine_label} ''' # Truncate output for display @@ -176,9 +180,13 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None): output_link = build_snapshot_url(snapshot_id, embed_path, config=config) else: output_link = build_snapshot_url(snapshot_id, "", config=config) + output_link_attr = html.escape(output_link, quote=True) # Get version - try cmd_version field - version = result.cmd_version if result.cmd_version else "-" + version = html.escape(str(result.cmd_version if result.cmd_version else "-"), quote=True) + plugin_text = html.escape(str(result.plugin or ""), quote=True) + status_text = html.escape(str(status), quote=True) + pwd_text = html.escape(str(result.pwd or "-"), quote=True) # Unique ID for this row's expandable output row_id = f"output_{idx}_{str(result.id)[:8]}" @@ -195,18 +203,18 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None): {status} + color: {color}; background: {bg};">{status_text} - + {icon} - - {result.plugin} + {plugin_text} @@ -233,7 +241,7 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None):
- 📄 ID: {str(result.id)} Version: {version} - PWD: {result.pwd or "-"} + PWD: {pwd_text}
Output: diff --git a/archivebox/core/models.py b/archivebox/core/models.py index 6421beb5..f1d40eba 100755 --- a/archivebox/core/models.py +++ b/archivebox/core/models.py @@ -36,7 +36,7 @@ from archivebox.misc.util import ( to_json, ts_to_date_str, urlencode, - htmlencode, + htmldecode, sanitize_html_text, urldecode, validate_url, @@ -3434,8 +3434,8 @@ class Snapshot(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithConfig, ModelW context = { **self.to_dict(extended=True), "snapshot": self, - "title": htmlencode(self.resolved_title or (self.base_url if is_archived else TITLE_LOADING_MSG)), - "url_str": htmlencode(urldecode(self.base_url)), + "title": htmldecode(self.resolved_title or (self.base_url if is_archived else TITLE_LOADING_MSG)), + "url_str": htmldecode(urldecode(self.base_url)), "archive_url": urlencode(f"warc/{self.timestamp}" or (self.domain if is_archived else "")) or "about:blank", "extension": self.extension or "html", "tags": self.tags_str() or "untagged", diff --git a/archivebox/core/templatetags/core_tags.py b/archivebox/core/templatetags/core_tags.py index b142d095..4db3a639 100644 --- a/archivebox/core/templatetags/core_tags.py +++ b/archivebox/core/templatetags/core_tags.py @@ -560,6 +560,17 @@ def snapshot_index_row(context, link) -> str: f'' "" ) + elif "_public_preview_paths" in link.__dict__: + preview_paths = list(getattr(link, "_public_preview_paths", []) or []) + if preview_paths: + preview_urls = [build_snapshot_url(snapshot_id, path, request=request, config=config) for path in preview_paths] + preview_html = ( + f'' + ) + else: + preview_html = '' else: preview_html = ( f' 0: + preview_paths_by_snapshot[snapshot_key].append(f"{plugin}/{filename}") elif status == ArchiveResult.StatusChoices.FAILED: progress["failed"] += 1 elif status == ArchiveResult.StatusChoices.STARTED: @@ -1275,6 +1287,7 @@ class PublicIndexView(ListView): snapshot._icons_progress_stats = progress_by_snapshot.get(str(snapshot.id), {}) snapshot.num_outputs_cached = snapshot._icons_progress_stats.get("succeeded", 0) snapshot._tags_str_cached = ",".join(tag_names_by_snapshot.get(str(snapshot.id), [])) + snapshot._public_preview_paths = preview_paths_by_snapshot.get(str(snapshot.id), []) snapshot._is_archived_cached = bool(snapshot.downloaded_at or snapshot.status == Snapshot.StatusChoices.SEALED) context["object_list"] = snapshots return context diff --git a/archivebox/templates/core/public_index.html b/archivebox/templates/core/public_index.html index 11145ed4..217555f4 100644 --- a/archivebox/templates/core/public_index.html +++ b/archivebox/templates/core/public_index.html @@ -656,18 +656,22 @@ return url } + function replaceRegionFromDocument(doc, selector) { + const next = doc.querySelector(selector) + const current = document.querySelector(selector) + if (next && current) current.innerHTML = next.innerHTML + } + async function renderNow() { const url = buildListUrl() const response = await fetch(url, {headers: {'X-Requested-With': 'XMLHttpRequest'}, signal: controller.signal}) const html = await response.text() const doc = new DOMParser().parseFromString(html, 'text/html') - const nextList = doc.querySelector('.public-snapshot-list') - const currentList = document.querySelector('.public-snapshot-list') - if (nextList && currentList) { - currentList.replaceWith(nextList) - initPublicSearchMode() - setSearchLoading(controller, resultVersion ? 'Searching... ' + resultVersion + ' found' : 'Searching...') - } + replaceRegionFromDocument(doc, '.public-snapshot-count') + replaceRegionFromDocument(doc, '#table-bookmarks thead th:nth-child(3)') + replaceRegionFromDocument(doc, '#table-bookmarks tbody') + replaceRegionFromDocument(doc, '.public-pagination') + setSearchLoading(controller, resultVersion ? 'Searching... ' + resultVersion + ' found' : 'Searching...') } const listUrl = buildListUrl() diff --git a/archivebox/templates/core/snapshot.html b/archivebox/templates/core/snapshot.html index 69eedd85..3b5856a8 100644 --- a/archivebox/templates/core/snapshot.html +++ b/archivebox/templates/core/snapshot.html @@ -1151,7 +1151,7 @@
Favicon - {{title|truncatechars:120|safe}} + {{title|truncatechars:120}} {% if title_tags %} {{ snapshot_permissions_icon }} {{ snapshot_permissions }} diff --git a/archivebox/tests/test_search.py b/archivebox/tests/test_search.py index ec5e1483..e1179895 100644 --- a/archivebox/tests/test_search.py +++ b/archivebox/tests/test_search.py @@ -479,6 +479,15 @@ class TestPublicIndexSearch: assert response.status_code == 200 assert b"archivebox-search-stream-status" in response.content + @override_settings(PUBLIC_INDEX=True) + def test_public_search_stream_preserves_search_form_dom(self, client, public_snapshot): + response = client.get("/public/", {"q": "Public Example"}, HTTP_HOST=WEB_HOST) + + assert response.status_code == 200 + assert b"replaceRegionFromDocument(doc, '.public-snapshot-count')" in response.content + assert b"replaceRegionFromDocument(doc, '#table-bookmarks tbody')" in response.content + assert b"currentList.replaceWith(nextList)" not in response.content + @override_settings(PUBLIC_INDEX=True) def test_public_search_stream_populates_public_results_cache(self, client, public_snapshot): search_params = {"q": "Public Example", "search_mode": "meta"} @@ -581,12 +590,21 @@ class TestPublicIndexSearch: assert response.status_code == 200 content = response.content.decode() - assert "screenshot/screenshot.png" in content first = content.index("chrome_extension_screenshot/screenshot-1.png") second = content.index("chrome_extension_screenshot/screenshot.png") assert first < second + assert "screenshot/screenshot.png" not in content assert "chrome_extension_screenshot/screenshot-2.png" not in content + @override_settings(PUBLIC_INDEX=True) + def test_public_index_snapshot_without_preview_renders_placeholder(self, client, public_snapshot): + response = client.get("/public/", HTTP_HOST=WEB_HOST) + + assert response.status_code == 200 + content = response.content.decode() + assert "snapshot-preview-empty" in content + assert "screenshot/screenshot.png" not in content + @override_settings(PUBLIC_INDEX=True) def test_public_index_pending_snapshot_uses_small_preview_spinner(self, client, crawl): from archivebox.core.models import Snapshot diff --git a/archivebox/tests/test_ui_admin_snapshot.py b/archivebox/tests/test_ui_admin_snapshot.py index 19883f5a..2597ff70 100644 --- a/archivebox/tests/test_ui_admin_snapshot.py +++ b/archivebox/tests/test_ui_admin_snapshot.py @@ -71,6 +71,72 @@ def test_snapshot_admin_archive_results_escape_extractor_output(admin_client, sn assert b"<img src=x onerror="window.__archivebox_archiveresult_xss__=1">" in body +def test_snapshot_admin_archive_result_table_escapes_legacy_string_fields(admin_client, snapshot): + from uuid import uuid4 + + from archivebox.core.models import ArchiveResult + from archivebox.machine.models import Binary, Machine, Process + + machine = Machine.objects.create( + guid=f"xss-machine-{uuid4()}", + hostname='', + hw_in_docker=False, + hw_in_vm=False, + hw_manufacturer="Test", + hw_product="Test Product", + hw_uuid=f"xss-hw-{uuid4()}", + os_arch="arm64", + os_family="darwin", + os_platform="macOS", + os_release="14.0", + os_kernel="Darwin", + stats={}, + config={}, + ) + binary = Binary.objects.create( + machine=machine, + name="staticfile", + abspath="/usr/bin/staticfile", + version='v', + binprovider="env", + binproviders="env", + status=Binary.StatusChoices.INSTALLED, + ) + process = Process.objects.create( + machine=machine, + binary=binary, + process_type=Process.TypeChoices.HOOK, + pwd='/tmp/archivebox">', + cmd=["staticfile"], + status=Process.StatusChoices.EXITED, + ) + result = ArchiveResult.objects.create( + snapshot=snapshot, + plugin="staticfile", + hook_name="on_Snapshot__00_staticfile.py", + process=process, + status=ArchiveResult.StatusChoices.SUCCEEDED, + output_files={'evil">.txt': {"size": 12, "mimetype": "text/plain"}}, + output_str='staticfile/evil">.txt', + ) + ArchiveResult.objects.filter(pk=result.pk).update(plugin='') + + response = admin_client.get(reverse("admin:core_snapshot_change", args=[snapshot.pk]), HTTP_HOST=ADMIN_TEST_HOST) + body = response.content + + assert response.status_code == 200 + assert b'" + title_payload = "Legacy title 1 < 2 & 3 " tag_payload = "" url_payload = "https://public-xss.example/" + filename_payload = 'evil">.txt' Snapshot.objects.filter(pk=snapshot.pk).update(title=title_payload, url=url_payload) Tag.objects.filter(pk=tag.pk).update(name=tag_payload) + ArchiveResult.objects.create( + snapshot=snapshot, + plugin="staticfile", + hook_name="on_Snapshot__00_staticfile.py", + status=ArchiveResult.StatusChoices.SUCCEEDED, + output_files={filename_payload: {"size": 12, "mimetype": "text/plain"}}, + ) public_index = client.get("/public/", HTTP_HOST=WEB_TEST_HOST) snapshot_detail = client.get(f"/{snapshot_archive_path}/index.html", HTTP_HOST=WEB_TEST_HOST) @@ -311,9 +319,13 @@ class TestPublicIndex: assert b"