Handle untrusted uploads¶
Reports often come from outside: an upload form, a supervisor's portal, an e-mail attachment. dpm-xbrl is built to read them safely. This guide shows the settings that matter; the security model explains what is protected and why.
Read bytes, catch ReportError¶
Read the upload's bytes directly, with name= set to the uploaded file
name, and catch ReportError. Every problem with the
input raises it, so one handler covers malformed files, unsafe archives and
inputs over a limit:
from dpmxbrl import Limits, ReportError, read_report
def accept(data: bytes, filename: str) -> str:
try:
report = read_report(data, name=filename, limits=Limits(max_facts=1_000_000))
except ReportError as error:
return f"rejected: {error}"
errors = [finding for finding in report.filing if finding.severity == "error"]
return f"accepted: {len(report.facts)} facts, {len(errors)} filing errors"
with open("report.zip", "rb") as upload:
print(accept(upload.read(), "report.zip"))
print(accept(b"PK\x03\x04 truncated", "broken.zip"))
print(
accept(
b'<?xml version="1.0"?><!DOCTYPE x [<!ENTITY e SYSTEM "file:///etc/passwd">]><x>&e;</x>',
"evil.xbrl",
)
)
accepted: 14 facts, 0 filing errors
rejected: broken.zip is not a readable ZIP archive: it may be incomplete or damaged
rejected: evil.xbrl: DOCTYPE and ENTITY declarations are not allowed
Messages name the file and, where known, the line, and are safe to show to the person who uploaded it. They contain no stack traces and no paths other than the names inside the upload.
Set the limits¶
Limits caps the resources one read may use. The
defaults suit reports from regular reporting; tighten them for a public
upload form:
| Field | Default | Caps |
|---|---|---|
max_zip_entries |
10,000 | entries in a report package ZIP |
max_total_size |
2 GiB | total uncompressed size of a package |
max_file_size |
512 MiB | uncompressed size of one file, including an XML instance and files read from taxonomy packages |
max_facts |
20,000,000 | facts in one report |
max_extends_depth |
32 | nesting of xBRL-CSV extends |
max_taxonomy_entries |
100,000 | entries in one taxonomy package ZIP (the EBA's reporting frameworks package has about 17,400) |
Each limit raises ReportError when exceeded, before the data is inflated
or the facts are built:
for limits in (
Limits(max_facts=10),
Limits(max_zip_entries=5),
Limits(max_file_size=100),
Limits(max_total_size=500),
):
try:
read_report("report.zip", limits=limits)
except ReportError as error:
print(error)
reports/c_28.00.csv:2: report exceeds the limit of 10 facts
report.zip: 8 ZIP entries exceed the limit of 5
report.zip: entry 'report/reports/report.json' exceeds the maximum file size of 100 bytes
report.zip: uncompressed size exceeds the limit of 500 bytes
Limits is a frozen dataclass; derive one from the defaults with
dataclasses.replace(Limits(), max_facts=1_000_000).
Raise the limits for large trusted reports¶
The largest EBA packages hold tens of millions of facts in files over
512 MiB. For reports you trust, raise the limits; from the command line,
--max-facts, --max-file-size, --max-total-size and
--max-zip-entries do the same, and a limit error names the option:
$ dpm-xbrl inspect report.zip --max-facts 10
error: reports/c_28.00.csv:2: report exceeds the limit of 10 facts
hint: --max-facts raises the limit
$ echo $?
1
A 21.9-million-fact package with one 587 MB table needs
--max-file-size 1000000000 --max-total-size 4000000000 --max-facts 30000000.
What else is rejected¶
Report packages are rejected for entry names that are absolute or contain
.., duplicate entries and encrypted entries; package directories for
links that leave the directory. XML instances are scanned before parsing,
and any DOCTYPE, and with it any entity declaration, is rejected. Nothing
is ever fetched over the network.
import io
from zipfile import ZipFile
buffer = io.BytesIO()
with ZipFile(buffer, "w") as archive:
archive.writestr("../evil/reports/report.json", "{}")
try:
read_report(buffer.getvalue(), name="upload.zip")
except ReportError as error:
print(error)
Run reads in a separate process¶
Limits bound one read, not the whole process. For a public service, run reads in a worker process with an operating-system memory limit and a timeout as well, so one hostile upload cannot slow other requests. Readers pause the cyclic garbage collector for the whole process while they build facts, which is another reason to keep them out of a latency-sensitive server process.