summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2025-09-10 16:15:28 +0100
committerMichael Williamson <mike@zwobble.org>2025-09-10 16:15:28 +0100
commit150bf98e7ae2fd8ae874c3e756c39f01b3c01a23 (patch)
tree22844fd319ceba4c54d4e95ddb6246fd2e9b17d2
parent603f8b83c0389be91ca90a3cb7690d7f237699d8 (diff)
Support disabling external file access
-rw-r--r--NEWS2
-rw-r--r--README.md13
-rw-r--r--mammoth/__init__.py19
-rw-r--r--mammoth/docx/__init__.py10
-rw-r--r--mammoth/docx/files.py14
-rw-r--r--tests/docx/files_tests.py21
-rw-r--r--tests/mammoth_tests.py8
7 files changed, 73 insertions, 14 deletions
diff --git a/NEWS b/NEWS
index af6ce0b..b4003c2 100644
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,8 @@
* Fix conversion of unmerged table cells.
+* Support disabling external file accesses using the external_file_access argument.
+
# 1.10.0
* Add "Heading" and "Body" styles, as found in documents created by Apple Pages,
diff --git a/README.md b/README.md
index adc4b2f..82f4c24 100644
--- a/README.md
+++ b/README.md
@@ -178,6 +178,16 @@ pass `include_default_style_map=False`:
result = mammoth.convert_to_html(docx_file, style_map=style_map, include_default_style_map=False)
```
+#### External file access
+
+Source documents may reference files outside of the source document.
+To disable access to any such external files during the conversion process,
+pass `external_file_access=False`:
+
+```javascript
+result = mammoth.convert_to_html(docx_file, external_file_access=False)
+```
+
#### Custom image handlers
By default, images are converted to `<img>` elements with the source included inline in the `src` attribute.
@@ -415,6 +425,9 @@ For instance:
and embed the HTML into your website,
this may allow arbitrary files on the server to be read and exfiltrated.
+ To disable access to any such external files during the conversion process,
+ pass `external_file_access=False`.
+
### Document transforms
**The API for document transforms should be considered unstable,
diff --git a/mammoth/__init__.py b/mammoth/__init__.py
index 27cc580..e8d3a83 100644
--- a/mammoth/__init__.py
+++ b/mammoth/__init__.py
@@ -16,15 +16,28 @@ def convert_to_markdown(*args, **kwargs):
return convert(*args, output_format="markdown", **kwargs)
-def convert(fileobj, transform_document=None, id_prefix=None, include_embedded_style_map=_undefined, **kwargs):
+def convert(
+ fileobj,
+ transform_document=None,
+ id_prefix=None,
+ include_embedded_style_map=_undefined,
+ external_file_access=_undefined,
+ **kwargs
+):
if include_embedded_style_map is _undefined:
include_embedded_style_map = True
+
if transform_document is None:
transform_document = lambda x: x
+
if include_embedded_style_map:
kwargs["embedded_style_map"] = read_style_map(fileobj)
+
+ if external_file_access is _undefined:
+ external_file_access = True
+
return options.read_options(kwargs).bind(lambda convert_options:
- docx.read(fileobj).map(transform_document).bind(lambda document:
+ docx.read(fileobj, external_file_access=external_file_access).map(transform_document).bind(lambda document:
conversion.convert_document_element_to_html(
document,
id_prefix=id_prefix,
@@ -32,7 +45,7 @@ def convert(fileobj, transform_document=None, id_prefix=None, include_embedded_s
)
)
)
-
+
def extract_raw_text(fileobj):
return docx.read(fileobj).map(extract_raw_text_from_element)
diff --git a/mammoth/docx/__init__.py b/mammoth/docx/__init__.py
index 3848736..a0576cf 100644
--- a/mammoth/docx/__init__.py
+++ b/mammoth/docx/__init__.py
@@ -19,13 +19,14 @@ from ..zips import open_zip
_empty_result = results.success([])
-def read(fileobj):
+def read(fileobj, external_file_access=False):
zip_file = open_zip(fileobj, "r")
part_paths = _find_part_paths(zip_file)
read_part_with_body = _part_with_body_reader(
getattr(fileobj, "name", None),
zip_file,
part_paths=part_paths,
+ external_file_access=external_file_access,
)
return results.combine([
@@ -134,7 +135,7 @@ def _read_document(zip_file, read_part_with_body, notes, comments, part_paths):
)
-def _part_with_body_reader(document_path, zip_file, part_paths):
+def _part_with_body_reader(document_path, zip_file, part_paths, external_file_access):
content_types = _try_read_entry_or_default(
zip_file,
"[Content_Types].xml",
@@ -156,7 +157,10 @@ def _part_with_body_reader(document_path, zip_file, part_paths):
default=Numbering.EMPTY,
)
- files = Files(None if document_path is None else os.path.dirname(document_path))
+ files = Files(
+ None if document_path is None else os.path.dirname(document_path),
+ external_file_access=external_file_access,
+ )
def read_part(name, reader, default=_undefined):
relationships = _read_relationships(zip_file, _find_relationships_path_for(name))
diff --git a/mammoth/docx/files.py b/mammoth/docx/files.py
index 61cf8d5..e26a43f 100644
--- a/mammoth/docx/files.py
+++ b/mammoth/docx/files.py
@@ -11,10 +11,16 @@ except ImportError:
class Files(object):
- def __init__(self, base):
+ def __init__(self, base, external_file_access):
self._base = base
-
+ self._external_file_access = external_file_access
+
def open(self, uri):
+ if not self._external_file_access:
+ raise ExternalFileAccessIsDisabledError(
+ "could not open external image '{0}', external file access is disabled".format(uri)
+ )
+
try:
if _is_absolute(uri):
return contextlib.closing(urlopen(uri))
@@ -34,3 +40,7 @@ def _is_absolute(url):
class InvalidFileReferenceError(ValueError):
pass
+
+
+class ExternalFileAccessIsDisabledError(InvalidFileReferenceError):
+ pass
diff --git a/tests/docx/files_tests.py b/tests/docx/files_tests.py
index 6845cc2..b405373 100644
--- a/tests/docx/files_tests.py
+++ b/tests/docx/files_tests.py
@@ -1,10 +1,19 @@
-from mammoth.docx.files import Files, InvalidFileReferenceError
+from mammoth.docx.files import ExternalFileAccessIsDisabledError, Files, InvalidFileReferenceError
from ..testing import generate_test_path, assert_equal, assert_raises
+def test_when_external_file_access_is_disabled_then_opening_file_raises_error():
+ files = Files(None, external_file_access=False)
+ error = assert_raises(ExternalFileAccessIsDisabledError, lambda: files.open("/tmp/image.png"))
+ expected_message = (
+ "could not open external image '/tmp/image.png', external file access is disabled"
+ )
+ assert_equal(expected_message, str(error))
+
+
def test_can_open_files_with_file_uri():
path = generate_test_path("tiny-picture.png")
- files = Files(None)
+ files = Files(None, external_file_access=True)
with files.open("file:///" + path) as image_file:
contents = image_file.read()
assert_equal(bytes, type(contents))
@@ -13,7 +22,7 @@ def test_can_open_files_with_file_uri():
def test_can_open_files_with_relative_uri():
- files = Files(generate_test_path(""))
+ files = Files(generate_test_path(""), external_file_access=True)
with files.open("tiny-picture.png") as image_file:
contents = image_file.read()
assert_equal(bytes, type(contents))
@@ -22,7 +31,7 @@ def test_can_open_files_with_relative_uri():
def test_given_base_is_not_set_when_opening_relative_uri_then_error_is_raised():
- files = Files(None)
+ files = Files(None, external_file_access=True)
error = assert_raises(InvalidFileReferenceError, lambda: files.open("not-a-real-file.png"))
expected_message = (
"could not find external image 'not-a-real-file.png', fileobj has no name"
@@ -31,7 +40,7 @@ def test_given_base_is_not_set_when_opening_relative_uri_then_error_is_raised():
def test_error_is_raised_if_relative_uri_cannot_be_opened():
- files = Files("/tmp")
+ files = Files("/tmp", external_file_access=True)
error = assert_raises(InvalidFileReferenceError, lambda: files.open("not-a-real-file.png"))
expected_message = (
"could not open external image: 'not-a-real-file.png' (document directory: '/tmp')\n" +
@@ -41,7 +50,7 @@ def test_error_is_raised_if_relative_uri_cannot_be_opened():
def test_error_is_raised_if_file_uri_cannot_be_opened():
- files = Files("/tmp")
+ files = Files("/tmp", external_file_access=True)
error = assert_raises(InvalidFileReferenceError, lambda: files.open("file:///not-a-real-file.png"))
expected_message = "could not open external image: 'file:///not-a-real-file.png' (document directory: '/tmp')\n"
assert str(error).startswith(expected_message)
diff --git a/tests/mammoth_tests.py b/tests/mammoth_tests.py
index 36d0e83..a7308f8 100644
--- a/tests/mammoth_tests.py
+++ b/tests/mammoth_tests.py
@@ -127,6 +127,14 @@ def test_warn_if_images_stored_outside_of_document_are_specified_when_passing_fi
assert_equal([results.warning("could not find external image 'tiny-picture.png', fileobj has no name")], result.messages)
+def test_warn_if_images_stored_outside_of_document_are_specified_when_external_file_access_is_disabled():
+ with open(generate_test_path("external-picture.docx"), "rb") as fileobj:
+ result = mammoth.convert_to_html(fileobj=fileobj, external_file_access=False)
+
+ assert_equal("", result.value)
+ assert_equal([results.warning("could not open external image 'tiny-picture.png', external file access is disabled")], result.messages)
+
+
def test_warn_if_images_stored_outside_of_document_are_not_found():
with tempman.create_temp_dir() as temp_dir:
document_path = os.path.join(temp_dir.path, "document.docx")