CVE-2021-29421 (published 2021-04-01T20:15Z)

models/metadata.py in the pikepdf package 1.3.0 through 2.9.2 for Python allows XXE when parsing XMP metadata entries.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-611
cwe_name: Improper Restriction of XML External Entity Reference
description: The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.
extended_description: 
            XML documents optionally contain a Document Type Definition (DTD), which, among other features, enables the definition of XML entities. It is possible to define an entity by providing a substitution string in the form of a URI. The XML parser can access the contents of this URI and embed these contents back into the XML document for further processing.
            By submitting an XML file that defines an external entity with a file:// URI, an attacker can cause the processing application to read the contents of a local file. For example, a URI such as "file:///c:/winnt/win.ini" designates (in Windows) the file C:\Winnt\win.ini, or file:///etc/passwd designates the password file in Unix-based systems. Using URIs with other schemes such as http://, the attacker can force the application to make outgoing requests to servers that the attacker cannot reach directly, which can be used to bypass firewall restrictions or hide the source of attacks such as port scanning.
            Once the content of the URI is read, it is fed back into the application that is processing the XML. This application may echo back the data (e.g. in an error message), thereby exposing the file contents.
         
         

Fix information:
repo_url: https://github.com/pikepdf/pikepdf
commit_details: repo_url: https://github.com/pikepdf/pikepdf
author: James R. Barlow
author_date: 2021-03-27 00:43:21-07:00
msg: Fix XXE vulnerability in XMP metadata parsing

For details:
https://portswigger.net/web-security/xxe

Reported by: Eric Therond eric.therond@sonarsource.com) of Sonarsource (https://www.sonarsource.com/)
repository: repo_url: https://github.com/pikepdf/pikepdf
repo_name: pikepdf/pikepdf
description: A Python library for reading and writing PDF, powered by QPDF
repo_language: Python
owner: pikepdf
file_changes: filename: _xml.py
change_type: ModificationType.ADD
diff: @@ -0,0 +1,30 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# Copyright (C) 2021, James R. Barlow (https://github.com/jbarlow83/)
+
+
+from typing import IO, Any, AnyStr, Union
+
+from lxml.etree import XMLParser as _UnsafeXMLParser
+from lxml.etree import parse as _parse
+
+
+class _XMLParser(_UnsafeXMLParser):
+    def __init__(self, *args, **kwargs):
+        # Prevent XXE attacks
+        # https://rules.sonarsource.com/python/type/Vulnerability/RSPEC-2755
+        kwargs['resolve_entities'] = False
+        kwargs['no_network'] = True
+        super().__init__(*args, **kwargs)
+
+
+def parse_xml(source: Union[AnyStr, IO[Any]], recover: bool = False):
+    """Wrapper around lxml's parse to provide protection against XXE attacks."""
+
+    parser = _XMLParser(recover=recover, remove_pis=False)
+    return _parse(source, parser=parser)
+
+
+__all__ = ['parse_xml']

programming_language: Python
filename: metadata.py
change_type: ModificationType.MODIFY
diff: @@ -26,10 +26,11 @@ from typing import (
 from warnings import warn
 
 from lxml import etree
-from lxml.etree import QName, XMLParser, XMLSyntaxError, parse
+from lxml.etree import QName, XMLSyntaxError
 
 from .. import Name, Stream, String
 from .. import __version__ as pikepdf_version
+from .._xml import parse_xml
 
 if sys.version_info < (3, 9):  # pragma: no cover
     from typing import Iterable, MutableMapping
@@ -413,14 +414,13 @@ class PdfMetadata(MutableMapping):
             data = XMP_EMPTY  # on some platforms lxml chokes on empty documents
 
         def basic_parser(xml):
-            return parse(BytesIO(xml))
+            return parse_xml(BytesIO(xml))
 
         def strip_illegal_bytes_parser(xml):
-            return parse(BytesIO(re_xml_illegal_bytes.sub(b'', xml)))
+            return parse_xml(BytesIO(re_xml_illegal_bytes.sub(b'', xml)))
 
         def recovery_parser(xml):
-            parser = XMLParser(recover=True)
-            return parse(BytesIO(xml), parser)
+            return parse_xml(BytesIO(xml), recover=True)
 
         def replace_with_empty_xmp(_xml=None):
             log.warning("Error occurred parsing XMP, replacing with empty XMP.")

programming_language: Python
filename: test_metadata.py
change_type: ModificationType.MODIFY
diff: @@ -729,3 +729,27 @@ def test_exception_undoes_edits(graph):
             raise
         m = graph.open_metadata()
         assert m['dc:format'] != 'application/pdf-demo'
+
+
+def test_xxe(trivial, outdir):
+    secret = outdir / 'secret.txt'
+    secret.write_text("This is a secret")
+    trivial.Root.Metadata = Stream(
+        trivial,
+        b"""\
+<?xpacket begin='\xef\xbb\xbf' id='W5M0MpCehiHzreSzNTczkc9d'?>
+<!DOCTYPE rdf:RDF [<!ENTITY xxe SYSTEM "file://%s">]>
+<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image'>
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<note>
+<to>&xxe;</to>
+<from>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</from>
+</note>
+</rdf:RDF>
+</x:xmpmeta>
+<?xpacket end='w'?>
+    """
+        % os.fsencode(secret),
+    )
+    with trivial.open_metadata() as m:
+        assert 'This is a secret' not in str(m)

programming_language: Python