Package: lintian
Version: 2.138.0
Severity: important

Dear maintainers,

lintian hangs (runs into an infinite loop) on
https://deb.debian.org/debian/pool/main/s/simplesamlphp/simplesamlphp_2.3.11-1.dsc

I dug into this (diclaimer: using AI) and it sounds like this is due to
insecure parsing of XML files in the dep5 check. (second disclaimer: I
would not have been able to find this myself, I think). There's another
potential occurence of the issue in the appstream check.

This did not happen in previous versions of simplesamlphp because the
affected XML file was not included in the source package.

What follows is a AI-generated bug report. I reviewed it, but did not
modify it. I did not check the claim that it could also be used for
information disclosure.

- Lucas

================================================================

The `debian/copyright/dep5` check (and the identically-written appstream
check) parse arbitrary upstream-shipped XML files with `XML::LibXML`,
hardening the parser only with `no_network => 1`. That option maps to
libxml2's `XML_PARSE_NONET`, which blocks *network* (http/ftp) external
entities and DTDs but does NOT block local `file://` external entities.
Because the rest of the standard XXE hardening (`load_ext_dtd`,
`expand_entities`, `no_ent`, `huge_schema`) is not applied, any source
package that ships an attacker-controlled `.xml` file can make lintian:

  * hang indefinitely / exhaust CPU and RAM (denial of service), or
  * disclose locally-readable files (e.g. `/etc/passwd`) by injecting
    the result back into a tag that lintian inspects and reports.

A real-world source package already triggers the hang: lintian on
`simplesamlphp_2.3.11-1.dsc` never returns; CPU-bound with
`/dev/urandom` held open by the lintian process. (The fixture is an
upstream XXE test fixture pointing at `file:///dev/urandom`, which
libxml2 tries to fully read into the entity, streaming forever.)

Affected code paths (lintian 2.138.0)
-------------------------------------

Two `XML::LibXML` callers, both only call `set_option('no_network', 1)`:

1. `lib/Lintian/Check/Debian/Copyright/Dep5.pm:805-811`
   In `check_dep5_copyright`, the appstream-metadata-license loop
   parses *every shipped `*.xml`* in the unpacked source tree whose
   path appears in `%license_identifiers_by_file` (i.e. any shipped
   `.xml` under a `Files:` wildcard in `d/copyright`). This is the
   exact site of the simplesamlphp hang. It runs on source processables.

       my $parser = XML::LibXML->new;
       $parser->set_option('no_network', 1);
       my $file = $self->processable->patched->resolve_path($name);
       my $doc;
       try {
           $doc = $parser->parse_file($file->unpacked_path);
       } catch { next; };

2. `lib/Lintian/Check/AppstreamMetadata.pm:143-148`
   `check_modalias` parses each installed `*.metainfo.xml` /
   `usr/share/metainfo/*.xml` file (guarded only by an `is_open_ok`
   size/type check). Runs on binary (installable) processables. The
   XXE surface is smaller here in practice (appstream files in
   `/usr/share/metainfo` are usually small and well-formed), but the
   hardening gap is byte-identical to case 1 and exploitable by a
   malicious package author.

Both call sites have been confirmed byte-for-byte equivalent across all
lintian versions examined from 2.104.0 (Nov 2020) through 2.138.0 (Jul
2026); nothing in the XML hardening has ever been strengthened. Older
versions were installed from snapshot.debian.org and behaviourally
reproduced against the simplesamlphp 2.3.11-1 dsc (every runnable
version ≥ 2.114.0 hangs within a 45s timeout).

Reproduction
------------

Minimal XML::LibXML repro (mirrors the dep5 parser options):

    $ cat xxe.xml
    <?xml version="1.0" encoding="ISO-8859-1"?>
        <!DOCTYPE foo [<!ELEMENT foo ANY > <!ENTITY xxe SYSTEM 
"file:///dev/urandom" >]>
    <foo>&xxe;</foo>

    $ cat repro.pl
    use XML::LibXML;
    my $p = XML::LibXML->new;
    $p->set_option("no_network", 1);
    $doc = $p->parse_file($ARGV[0]);

    $ timeout 8 perl repro.pl xxe.xml; echo "exit=$?"
    exit=124

End-to-end on Debian (current trixie):

    $ wget http://deb.debian.org/.../simplesamlphp_2.3.11-1.dsc [...]
    $ lintian --verbose simplesamlphp_2.3.11-1.dsc
    ...
    Running check: debian/copyright/dep5 on source:simplesamlphp_2.3.11-1 ...
    [HANGS — CPU ~60-85%, /dev/urandom fd open]

The fixture being ingested is
`vendor/simplesamlphp/xml-common/resources/xml/domdocument_doctype.xml`
shipped in the upstream `simplesamlphp/xml-common` composer dependency
(included in Debian's 2.3.11-1 orig tarball). The previous Debian version
`simplesamlphp_2.2.6-1` does *not* ship `xml-common`, so it does not trigger
the bug; lintian finishes on it in ~12s, exit 0.

Note that `file:///dev/urandom` is the *least* dangerous payload here.
`file:///etc/passwd`, `file:///etc/shadow` (read as the invoking user),
or `file:///proc/self/environ` would be silently read by libxml2 and
inlined into the document; if anything in the new appstream loop inspects
the text content of `&xxe;`-bearing nodes it could leak into lintian's
output. Even without disclosure, a payload such as the "billion-laughs"
exponential entity expansion hangs without touching the filesystem at
all — `no_network` does nothing against purely internal entity bombs.

Suggested fix (Dep5.pm and AppstreamMetadata.pm)
-------------------------------------------------

Replace the parser construction in both call sites with a fully hardened
one:

    my $parser = XML::LibXML->new;
    $parser->set_option('no_network', 1);
    $parser->set_option('expand_entities', 0);   # block external entity 
expansion
    $parser->set_option('load_ext_dtd', 0);      # do not even load the DOCTYPE 
subset
    $parser->set_option('huge_schema', 0);        # reject pathologically large 
DTDs
    # equivalently:
    # $parser->set_option('recover', 2) and do NOT set XML_PARSE_NOENT
    # or use XML::LibXML->new->{expand_xinclude} = 0 as well

Setting `expand_entities => 0` alone is sufficient to stop the
`/dev/urandom` infinite read (verified with a `safe.pl` repro that
returns in 0.048s, exit 0). The combination above additionally
mitigates billion-laughs, XXE file disclosure, and XInclude expansion.

For consistency it would be ideal to centralise the parser construction
into a small helper in `Lintian::Util` (or a new `Lintian::XML`) so that
both call sites — and any future XML consumer — share a single hardened
instantiation.

Reply via email to