Fix catching of defusedxml exception
This commit is contained in:
parent
d4fd42522d
commit
575c2b79f9
2 changed files with 56 additions and 0 deletions
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import argparse
|
||||
import sys
|
||||
from defusedxml import defuse_stdlib
|
||||
from defusedxml.common import DefusedXmlException
|
||||
from pathlib import Path
|
||||
|
||||
from . import j2
|
||||
|
|
@ -77,6 +78,20 @@ def _main(argv: list[str] | None = None) -> int:
|
|||
except OutputPathError as exc:
|
||||
print(f"jinjaturtle: refusing unsafe output path: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
except DefusedXmlException as exc:
|
||||
# defusedxml rejected the XML because it attempted a DTD, entity
|
||||
# expansion, or external reference (XXE / billion-laughs class attack).
|
||||
# This is a deliberately-blocked attack, not a benign malformed file, so
|
||||
# core.parse_config lets it propagate unchanged rather than folding it
|
||||
# into ConfigParseError. Report it as a refused unsafe input with a
|
||||
# non-zero exit code instead of leaking an internal traceback.
|
||||
print(
|
||||
"jinjaturtle: refusing unsafe XML input: the document uses a DTD, "
|
||||
f"entity expansion, or external reference ({exc.__class__.__name__}). "
|
||||
"This is blocked to prevent XXE / entity-expansion attacks.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
except ConfigParseError as exc:
|
||||
# The source file could not be parsed as its (detected or forced)
|
||||
# format. This is expected for malformed/attacker-influenced input;
|
||||
|
|
|
|||
|
|
@ -106,6 +106,47 @@ def test_cli_fails_cleanly_on_malformed_xml(tmp_path: Path) -> None:
|
|||
assert "Traceback (most recent call last)" not in res.stderr
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"content",
|
||||
[
|
||||
# Classic XXE: external SYSTEM entity used to read a local file.
|
||||
(
|
||||
'<?xml version="1.0"?>\n'
|
||||
'<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/hostname">]>\n'
|
||||
"<root>&xxe;</root>\n"
|
||||
),
|
||||
# Billion-laughs style internal entity expansion.
|
||||
(
|
||||
'<?xml version="1.0"?>\n'
|
||||
'<!DOCTYPE lolz [<!ENTITY lol "lol"><!ENTITY lol2 "&lol;&lol;&lol;">]>\n'
|
||||
"<lolz>&lol2;</lolz>\n"
|
||||
),
|
||||
# External parameter entity (SSRF / out-of-band XXE vector).
|
||||
(
|
||||
'<?xml version="1.0"?>\n'
|
||||
'<!DOCTYPE root [<!ENTITY % ext SYSTEM "http://127.0.0.1:9/x.dtd"> %ext;]>\n'
|
||||
"<root>x</root>\n"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_cli_refuses_xxe_cleanly(tmp_path: Path, content: str) -> None:
|
||||
"""defusedxml's security refusal must surface as a clean non-zero exit.
|
||||
|
||||
``EntitiesForbidden``/``DTDForbidden``/``ExternalReferenceForbidden`` subclass
|
||||
``DefusedXmlException`` (and ``ValueError``). ``parse_config`` deliberately
|
||||
lets them propagate rather than folding them into ``ConfigParseError`` so a
|
||||
blocked attack stays distinguishable from a benign malformed file. The CLI
|
||||
must therefore catch ``DefusedXmlException`` itself and exit cleanly instead
|
||||
of leaking a Python traceback.
|
||||
"""
|
||||
src = tmp_path / "xxe.xml"
|
||||
src.write_text(content, encoding="utf-8")
|
||||
res = _run_cli([str(src), "-f", "xml", "-r", "demo"])
|
||||
assert res.returncode == 2
|
||||
assert "refusing unsafe XML input" in res.stderr
|
||||
assert "Traceback (most recent call last)" not in res.stderr
|
||||
|
||||
|
||||
def test_folder_mode_skips_unparseable_file(tmp_path: Path) -> None:
|
||||
"""One malformed file should not abort processing of the whole directory."""
|
||||
from jinjaturtle.multi import process_directory
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue