Skip to content

Commit b10379c

Browse files
authored
Fix: plug security issue partition system files via include (#3908)
#### Summary A recent security review showed that it was possible to partition arbitrary local files in cases where the filetype supports an "include" functionality that brings in the content of files external to the partitioned file. This affects `rst` and `org` files. #### Fix This PR fixes the above issue by passing the parameter `sandbox=True` in all cases where `pypandoc.convert_file` is called. Note I also added the parameter to a call to this method in the ODT code. I haven't investigated whether there was a security issue with ODT files, but it seems better to use pandoc in sandbox mode given the security issues we know about. #### Testing To verify that the tests that are added with this PR find the relevant issue: - Remove the `sandbox=True` text from `unstructured/file_utils/file_conversion.py` line 17. - Run the tests `test_unstructured.partition.test_rst.test_rst_wont_include_external_files` and `test_unstructured.partition.test_org.test_org_wont_include_external_files`. Both should fail due to the partitioning containing the word "wombat", which only appears in a file external to the partitioned file. - Add the parameter back in, and the tests pass.
1 parent 5852260 commit b10379c

File tree

10 files changed

+114
-8
lines changed

10 files changed

+114
-8
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 0.16.20
2+
3+
### Enhancements
4+
5+
### Features
6+
7+
### Fixes
8+
- **Fix a security issue where rst and org files could read files in the local filesystem**. Certain filetypes could 'include' or 'import' local files into their content, allowing partitioning of arbitrary files from the local filesystem. Partitioning of these files is now sandboxed.
9+
110
## 0.16.19
211

312
### Enhancements

example-docs/README-w-include.org

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#+INCLUDE: "file_we_dont_want_imported"
2+
3+
* Example Docs
4+
5+
The sample docs directory contains the following files:
6+
7+
- ~example-10k.html~ - A 10-K SEC filing in HTML format
8+
- ~layout-parser-paper.pdf~ - A PDF copy of the layout parser paper
9+
- ~factbook.xml~ / ~factbook.xsl~ - Example XML/XLS files that you
10+
can use to test stylesheets
11+
12+
These documents can be used to test out the parsers in the library. In
13+
addition, here are instructions for pulling in some sample docs that are
14+
too big to store in the repo.
15+
16+
** XBRL 10-K
17+
18+
You can get an example 10-K in inline XBRL format using the following
19+
~curl~. Note, you need to have the user agent set in the header or the
20+
SEC site will reject your request.
21+
22+
#+BEGIN_SRC bash
23+
24+
curl -O \
25+
-A '${organization} ${email}'
26+
https://www.sec.gov/Archives/edgar/data/311094/000117184321001344/0001171843-21-001344.txt
27+
#+END_SRC
28+
29+
You can parse this document using the HTML parser.

example-docs/README-w-include.rst

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.. include:: file_we_dont_want_imported
2+
3+
Example Docs
4+
------------
5+
6+
The sample docs directory contains the following files:
7+
8+
- ``example-10k.html`` - A 10-K SEC filing in HTML format
9+
- ``layout-parser-paper.pdf`` - A PDF copy of the layout parser paper
10+
- ``factbook.xml``/``factbook.xsl`` - Example XML/XLS files that you
11+
can use to test stylesheets
12+
13+
These documents can be used to test out the parsers in the library. In
14+
addition, here are instructions for pulling in some sample docs that are
15+
too big to store in the repo.
16+
17+
XBRL 10-K
18+
^^^^^^^^^
19+
20+
You can get an example 10-K in inline XBRL format using the following
21+
``curl``. Note, you need to have the user agent set in the header or the
22+
SEC site will reject your request.
23+
24+
.. code:: bash
25+
26+
curl -O \
27+
-A '${organization} ${email}'
28+
https://www.sec.gov/Archives/edgar/data/311094/000117184321001344/0001171843-21-001344.txt
29+
30+
You can parse this document using the HTML parser.
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
wombat

test_unstructured/partition/test_org.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
4+
35
from pytest_mock import MockFixture
46

5-
from test_unstructured.unit_utils import assert_round_trips_through_JSON, example_doc_path
7+
from test_unstructured.unit_utils import (
8+
assert_round_trips_through_JSON,
9+
example_doc_path,
10+
find_text_in_elements,
11+
)
612
from unstructured.chunking.title import chunk_by_title
713
from unstructured.documents.elements import Title
814
from unstructured.partition.org import partition_org
@@ -138,3 +144,15 @@ def test_partition_org_respects_detect_language_per_element():
138144
)
139145
langs = [element.metadata.languages for element in elements]
140146
assert langs == [["eng"], ["spa", "eng"], ["eng"], ["eng"], ["spa"]]
147+
148+
149+
def test_org_wont_include_external_files():
150+
# Make sure our import file is in place (otherwise the import fails silently and test passes)
151+
assert Path(example_doc_path("file_we_dont_want_imported")).exists()
152+
elements = partition_org(example_doc_path("README-w-include.org"))
153+
# The partition should contain some elements
154+
assert elements
155+
# We find something we expect to find from file we partitioned directly
156+
assert find_text_in_elements("instructions", elements)
157+
# But we don't find something from the file included within the file we partitioned directly
158+
assert not find_text_in_elements("wombat", elements)

test_unstructured/partition/test_rst.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
4+
35
from pytest_mock import MockFixture
46

5-
from test_unstructured.unit_utils import assert_round_trips_through_JSON, example_doc_path
7+
from test_unstructured.unit_utils import (
8+
assert_round_trips_through_JSON,
9+
example_doc_path,
10+
find_text_in_elements,
11+
)
612
from unstructured.chunking.title import chunk_by_title
713
from unstructured.documents.elements import Title
814
from unstructured.partition.rst import partition_rst
@@ -120,3 +126,15 @@ def test_partition_rst_respects_detect_language_per_element():
120126
)
121127
langs = [element.metadata.languages for element in elements]
122128
assert langs == [["eng"], ["spa", "eng"], ["eng"], ["eng"], ["spa"]]
129+
130+
131+
def test_rst_wont_include_external_files():
132+
# Make sure our import file is in place (otherwise the import fails silently and test passes)
133+
assert Path(example_doc_path("file_we_dont_want_imported")).exists()
134+
elements = partition_rst(example_doc_path("README-w-include.rst"))
135+
# The partition should contain some elements
136+
assert elements
137+
# We find something we expect to find from file we partitioned directly
138+
assert find_text_in_elements("instructions", elements)
139+
# But we don't find something from the file included within the file we partitioned directly
140+
assert not find_text_in_elements("wombat", elements)

test_unstructured/unit_utils.py

+4
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,7 @@ def var_mock(request: FixtureRequest, q_var_name: str, **kwargs: Any) -> Mock:
257257
_patch = patch(q_var_name, **kwargs)
258258
request.addfinalizer(_patch.stop)
259259
return _patch.start()
260+
261+
262+
def find_text_in_elements(text: str, elements: List[Element]):
263+
return any(el.text.find(text) != -1 for el in elements)

unstructured/__version__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.16.19" # pragma: no cover
1+
__version__ = "0.16.20" # pragma: no cover

unstructured/file_utils/file_conversion.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def convert_file_to_text(filename: str, source_format: str, target_format: str)
1414
import pypandoc
1515

1616
try:
17-
text = pypandoc.convert_file(filename, target_format, format=source_format)
17+
text = pypandoc.convert_file(filename, target_format, format=source_format, sandbox=True)
1818
except FileNotFoundError as err:
1919
msg = (
2020
f"Error converting the file to text. Ensure you have the pandoc package installed on"

unstructured/partition/odt.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,7 @@ def _convert_odt_to_docx(
107107
import pypandoc
108108

109109
pypandoc.convert_file(
110-
source_file_path,
111-
"docx",
112-
format="odt",
113-
outputfile=target_docx_path,
110+
source_file_path, "docx", format="odt", outputfile=target_docx_path, sandbox=True
114111
)
115112

116113
return target_docx_path

0 commit comments

Comments
 (0)