Unsafe torch.load deserialization in load_pretrained_params() allows remote code execution
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 80/100
Research direction
Start in doctr/models/utils/pytorch.py at load_pretrained_params() and inspect the torch.load call and its supported PyTorch versions. The issue’s suggested change is to load with weights_only=True; done means the checkpoint load uses that option while retaining the existing map location.
Written by the indexing model from the issue text.
Description
Affected versions: confirmed on commit 11941d2bf1f77a496fda44272ef3a7ce3a8a0f45 (tag v1.1.0); the vulnerable pattern (torch.load without weights_only=True) has been present at this call site across docTR's history and applies to any install satisfying docTR's own torch>=2.0.0,<3.0.0 dependency bound using torch versions older than 2.6 (torch's own default for weights_only only flipped to True in 2.6).
Summary
doctr.models.utils.pytorch.load_pretrained_params() loads a model checkpoint with torch.load(archive_path, map_location="cpu"), without weights_only=True. On any torch release docTR currently supports (torch>=2.0.0,<3.0.0, i.e. anything before 2.6) this uses Python's pickle module with no restriction on what object graph may be constructed, so a crafted checkpoint file executes arbitrary code the moment it is loaded, before any state_dict key is ever checked. The same function accepts a URL, downloads it, and loads it the same way, and the library's own documentation presents loading a checkpoint from an arbitrary URL or local path as a normal usage pattern.
Details
doctr/models/utils/pytorch.py:
def load_pretrained_params(
model: nn.Module,
path_or_url: str | None = None,
hash_prefix: str | None = None,
ignore_keys: list[str] | None = None,
**kwargs: Any,
) -> None:
"""Load a set of parameters onto a model
>>> from doctr.models import load_pretrained_params
>>> load_pretrained_params(model, "https://yoursource.com/yourcheckpoint-yourhash.pt")
...
"""
if path_or_url is None:
logger.warning("No model URL or Path provided, using default initialization.")
return
archive_path = (
download_from_url(path_or_url, hash_prefix=hash_prefix, cache_subdir="models", **kwargs)
if validators.url(path_or_url)
else path_or_url
)
# Read state_dict
state_dict = torch.load(archive_path, map_location="cpu")
# Remove weights from the state_dict
if ignore_keys is not None and len(ignore_keys) > 0:
...
else:
# Load weights
model.load_state_dict(state_dict)
path_or_url is either a URL (downloaded via doctr.utils.data.download_from_url, which performs no code-level validation of the fetched bytes) or a local path, taken as-is. The hash_prefix integrity check in download_from_url is optional (None by default) and only verifies that a downloaded file's SHA256 matches a value the caller must already know; it does nothing to restrict what torch.load is allowed to deserialize, and is never consulted at all when path_or_url is a local filesystem path rather than a URL.
torch.load() without weights_only=True unpickles the file with the standard pickle module. Any object whose class defines __reduce__ runs that method's returned callable during unpickling, so a checkpoint can execute an arbitrary function (e.g. os.system) as a side effect of being loaded, independent of whatever model.load_state_dict() does afterward. The docstring on this exact function shows loading a checkpoint from a caller-supplied URL as the intended usage, so any downstream application that exposes "load a custom/community checkpoint" (a common feature request for libraries like this one: letting a user point at their own fine-tuned weights) hands that same code-execution primitive to whoever controls the file at that URL/path.
This is the same code path reached by doctr.contrib.base.py's _BaseContrib (loads an ONNX model via the same download_from_url, not the torch.load sink) and by doctr/datasets/datasets/base.py's dataset downloader, but the PyTorch checkpoint path is the one with a direct pickle-based remote-code-execution sink.
Steps To Reproduce
(available upon request)
Impact
Any application built on docTR that loads a checkpoint from a path or URL not fully controlled by the application operator (a "load your own fine-tuned model" feature, a shared/community checkpoint link, a file a user uploads) gives whoever controls that file arbitrary code execution in the process loading it, with no dependency on what the checkpoint's tensor contents are or whether load_state_dict() ever succeeds. This reaches the full privileges of the process calling load_pretrained_params(), confirmed as root in an isolated test container. The same sink is reachable through the library's documented URL-based usage, so a victim does not need to be tricked into running an unusual code path, only into pointing the library's own advertised feature at an attacker-controlled file.
CVSS 3.1: 8.8 (High) - AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-502: Deserialization of Untrusted Data
Suggested fix: pass weights_only=True to the torch.load() call at doctr/models/utils/pytorch.py:
- state_dict = torch.load(archive_path, map_location="cpu")
+ state_dict = torch.load(archive_path, map_location="cpu", weights_only=True)
- Dominant language
- Python
- Stars
- 6.4k
- Forks
- 684
- Avg merge
- 20h 28m
- Merged PRs (30d)
- 28
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from mindee/doctr
-
ext: tests topic: ci type: enhancement
Difficulty 3/5 Half a day Newbie friendliness 45/100
mindee/doctr#2171 · 1 comment ·
Maintainers usually reply within 1 day
-
Per-task YAML configuration for the reference training scriptsPossibly taken @felixdittrich92 claimed this 18 days ago. Openext: references topic: documentation type: new feature
mindee/doctr#2149 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
Release tracker - v1.2.0May be free again @felixdittrich92 claimed this 60 days ago, and no pull request is open. Opencritical
mindee/doctr#2126 · 1 assignee ·
Maintainers usually reply within 1 day
-
[table structure] Extend table structure datasetMay be free again @simont2k claimed this 81 days ago, and no pull request is open. Opentopic: table comprehension
mindee/doctr#2119 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
type: enhancement
Difficulty 5/5 Over a week Newbie friendliness 12/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
sierra-research/tau2-bench#635 ·
Maintainers usually reply within 1 day
-
The add-ssm-model skill requires a Cython simulator; the contributing guide allows Python onesPossibly taken @fmuia claimed this today. Opendocumentation skill
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
lnccbrown/ssm-simulators#406 ·
Maintainers usually reply within 4 days
-
Difficulty 1/5 Under an hour Newbie friendliness 95/100
btclib-org/btclib-ecc#309 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
jbacalso24/mimry#459 · 1 comment ·
Maintainers usually reply within 1 day
-
[Bug] POST /revoke with a FastMCP access token is a silent no-op (ownership gate never matches)Openauth bug high-priority
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day