[libvirt PATCH 0/4] network: reject line breaks in DNS records emitted to dnsmasq
The network XML schema supports typed DNS records using <dns><txt> and <dns><srv> tags, and the TXT and SRV domain/target values do not strip XML numeric char refs like (LF). As a result, these XML values can be used to insert additional configuration or commands into the dnsmasq config created by the network driver, e.g., a malicious dhcp-script=... line. libvirt socket access is already documented as root-equivalent, and raw <dnsmasq:options> namespace already exists, so this fix should be treated as hardening-only for default deployments that do not provide unpriv users with access to configure/extend these values. The series validates on both the parser and the emitter side: 1-2. reject LF/CR in the TXT value and SRV domain/target at XML parse time; 3. add a defensive check in the dnsmasq config emitter so a future parser gap cannot reintroduce directive injection; the raw <dnsmasq:options> namespace is intentionally left untouched; 4. add parse-path and update-API negative tests using / . Coordinated and assigned CVE-2026-61477 by RH team. Testing: built against current master with the network driver and tests enabled. The new negative tests fail on unpatched master (the expected parse error does not occur) and pass with the series applied, across both the network XML parse path and the update API. Michael Bommarito (4): conf: reject line breaks in DNS TXT record values conf: reject line breaks in DNS SRV domain and target network: reject line breaks before writing dnsmasq DNS config tests: cover line-break rejection in DNS TXT and SRV records src/conf/network_conf.c | 24 ++++++++++++++++ src/network/bridge_driver.c | 28 +++++++++++++++++++ .../dns-txt-record-newline.xml | 1 + .../srv-record-target-newline.xml | 1 + tests/networkxml2xmlupdatetest.c | 12 ++++++++ ...-network-dns-srv-record-domain-newline.xml | 14 ++++++++++ ...-network-dns-srv-record-target-newline.xml | 14 ++++++++++ .../nat-network-dns-txt-record-newline.xml | 14 ++++++++++ tests/networkxmlconftest.c | 3 ++ 9 files changed, 111 insertions(+) create mode 100644 tests/networkxml2xmlupdatein/dns-txt-record-newline.xml create mode 100644 tests/networkxml2xmlupdatein/srv-record-target-newline.xml create mode 100644 tests/networkxmlconfdata/nat-network-dns-srv-record-domain-newline.xml create mode 100644 tests/networkxmlconfdata/nat-network-dns-srv-record-target-newline.xml create mode 100644 tests/networkxmlconfdata/nat-network-dns-txt-record-newline.xml -- 2.53.0
The network XML schema exposes typed DNS records through the <dns><txt> element. The TXT value attribute accepts XML numeric character references, including (LF) and (CR), which survive attribute-value normalization. The network driver later writes the value verbatim into dnsmasq's line-oriented configuration file as a txt-record= line, so an embedded line break ends that directive and starts a new one under attacker control. This is a real boundary where a management layer permits editing typed DNS records while withholding the raw <dnsmasq:options> passthrough: the injected line escapes that restriction. Direct read-write access to the libvirt socket is already root-equivalent, so for the default deployment this is schema-correctness hardening. Add a helper that rejects LF and CR and call it for the TXT value during XML parsing. CVE-2026-61477 Fixes: 8b32c80df089 ("network: put dnsmasq parameters in conf-file instead of command line") Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com> --- src/conf/network_conf.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/conf/network_conf.c b/src/conf/network_conf.c index abd4c6eb4e..307ab0ae24 100644 --- a/src/conf/network_conf.c +++ b/src/conf/network_conf.c @@ -724,6 +724,23 @@ virNetworkDNSHostDefParseXML(const char *networkName, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" \ "_-+/*" + +static int +virNetworkDNSDefCheckLineBreaks(const char *record, + const char *field, + const char *value) +{ + if (virStringHasChars(value, "\r\n")) { + virReportError(VIR_ERR_XML_DETAIL, + _("invalid line break in DNS %1$s record %2$s attribute"), + record, field); + return -1; + } + + return 0; +} + + static int virNetworkDNSSrvDefParseXML(const char *networkName, xmlNodePtr node, @@ -852,6 +869,9 @@ virNetworkDNSTxtDefParseXML(const char *networkName, goto error; } + if (virNetworkDNSDefCheckLineBreaks("TXT", "value", def->value) < 0) + goto error; + if (!(def->name || def->value)) { virReportError(VIR_ERR_XML_DETAIL, _("Missing required name or value in DNS TXT record of network %1$s"), -- 2.53.0
The <dns><srv> domain and target attributes flow through the same dnsmasq configuration emitter as TXT values, written into srv-host= lines. Like the TXT value they accept XML numeric character references for LF and CR and are not otherwise constrained, unlike service and protocol which already have allow-lists. An embedded line break ends the srv-host= directive and begins a new one. Reject LF and CR in the SRV domain and target during XML parsing, reusing the helper added for TXT values. CVE-2026-61477 Fixes: 6612d1adb794 ("network: fix problems with SRV records") Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com> --- src/conf/network_conf.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/conf/network_conf.c b/src/conf/network_conf.c index 307ab0ae24..8eba1321d6 100644 --- a/src/conf/network_conf.c +++ b/src/conf/network_conf.c @@ -792,6 +792,10 @@ virNetworkDNSSrvDefParseXML(const char *networkName, def->domain = virXMLPropString(node, "domain"); def->target = virXMLPropString(node, "target"); + if (virNetworkDNSDefCheckLineBreaks("SRV", "domain", def->domain) < 0 || + virNetworkDNSDefCheckLineBreaks("SRV", "target", def->target) < 0) + goto error; + ret = virXPathUInt("string(./@port)", ctxt, &def->port); if (ret >= 0 && !def->target) { virReportError(VIR_ERR_XML_DETAIL, -- 2.53.0
The parser now rejects line breaks in typed DNS TXT and SRV fields, but the dnsmasq configuration emitter is the actual trust boundary: any future parser gap, or a value reaching the emitter by another path, would again let a typed DNS field inject an arbitrary dnsmasq directive. Add a defensive check in networkDnsmasqConfContents() that rejects LF and CR in every typed DNS string immediately before it is written to the line-based configuration file. This sits behind the parser checks and keeps the emitter correct on its own. The raw <dnsmasq:options> namespace is intentionally left untouched: it is the documented escape hatch for arbitrary dnsmasq options, and sanitizing it would be a separate, deliberate behavior change. CVE-2026-61477 Fixes: 8b32c80df089 ("network: put dnsmasq parameters in conf-file instead of command line") Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com> --- src/network/bridge_driver.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/network/bridge_driver.c b/src/network/bridge_driver.c index 4dc3e5424b..6ebdc27e76 100644 --- a/src/network/bridge_driver.c +++ b/src/network/bridge_driver.c @@ -64,6 +64,7 @@ #include "virhook.h" #include "virjson.h" #include "virnetworkportdef.h" +#include "virstring.h" #include "virutil.h" #include "virsystemd.h" #include "netdev_bandwidth_conf.h" @@ -119,6 +120,22 @@ networkDnsmasqDefNamespaceFree(void *nsdata) G_DEFINE_AUTOPTR_CLEANUP_FUNC(networkDnsmasqXmlNsDef, networkDnsmasqDefNamespaceFree); +static int +networkDnsmasqConfCheckLineBreaks(const char *record, + const char *field, + const char *value) +{ + if (virStringHasChars(value, "\r\n")) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, + _("DNS %1$s record %2$s must not contain line breaks"), + record, field); + return -1; + } + + return 0; +} + + static int networkDnsmasqDefNamespaceParseOptions(networkDnsmasqXmlNsDef *nsdef, xmlXPathContextPtr ctxt) @@ -1300,6 +1317,10 @@ networkDnsmasqConfContents(virNetworkObj *obj, if (wantDNS) { for (i = 0; i < dns->ntxts; i++) { + if (networkDnsmasqConfCheckLineBreaks("TXT", "name", dns->txts[i].name) < 0 || + networkDnsmasqConfCheckLineBreaks("TXT", "value", dns->txts[i].value) < 0) + return -1; + virBufferAsprintf(&configbuf, "txt-record=%s,%s\n", dns->txts[i].name, dns->txts[i].value); @@ -1321,6 +1342,13 @@ networkDnsmasqConfContents(virNetworkObj *obj, def->name); return -1; } + + if (networkDnsmasqConfCheckLineBreaks("SRV", "service", dns->srvs[i].service) < 0 || + networkDnsmasqConfCheckLineBreaks("SRV", "protocol", dns->srvs[i].protocol) < 0 || + networkDnsmasqConfCheckLineBreaks("SRV", "domain", dns->srvs[i].domain) < 0 || + networkDnsmasqConfCheckLineBreaks("SRV", "target", dns->srvs[i].target) < 0) + return -1; + /* RFC2782 requires that service and protocol be preceded by * an underscore. */ -- 2.53.0
Add negative tests that feed XML numeric character references for LF ( ) and CR ( ) into the DNS TXT value and SRV domain/target attributes, covering both the network XML parse path and the update API. Literal newlines are insufficient because XML parsers normalize raw attribute whitespace to spaces; the numeric references are what survive to the configuration emitter. CVE-2026-61477 Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com> --- .../dns-txt-record-newline.xml | 1 + .../srv-record-target-newline.xml | 1 + tests/networkxml2xmlupdatetest.c | 12 ++++++++++++ .../nat-network-dns-srv-record-domain-newline.xml | 14 ++++++++++++++ .../nat-network-dns-srv-record-target-newline.xml | 14 ++++++++++++++ .../nat-network-dns-txt-record-newline.xml | 14 ++++++++++++++ tests/networkxmlconftest.c | 3 +++ 7 files changed, 59 insertions(+) create mode 100644 tests/networkxml2xmlupdatein/dns-txt-record-newline.xml create mode 100644 tests/networkxml2xmlupdatein/srv-record-target-newline.xml create mode 100644 tests/networkxmlconfdata/nat-network-dns-srv-record-domain-newline.xml create mode 100644 tests/networkxmlconfdata/nat-network-dns-srv-record-target-newline.xml create mode 100644 tests/networkxmlconfdata/nat-network-dns-txt-record-newline.xml diff --git a/tests/networkxml2xmlupdatein/dns-txt-record-newline.xml b/tests/networkxml2xmlupdatein/dns-txt-record-newline.xml new file mode 100644 index 0000000000..13ed0408f4 --- /dev/null +++ b/tests/networkxml2xmlupdatein/dns-txt-record-newline.xml @@ -0,0 +1 @@ +<txt name='example' value='example value dhcp-script=/tmp/payload'/> diff --git a/tests/networkxml2xmlupdatein/srv-record-target-newline.xml b/tests/networkxml2xmlupdatein/srv-record-target-newline.xml new file mode 100644 index 0000000000..f29385d0e1 --- /dev/null +++ b/tests/networkxml2xmlupdatein/srv-record-target-newline.xml @@ -0,0 +1 @@ +<srv service='ldap' protocol='tcp' target='server.example.com dhcp-script=/tmp/payload'/> diff --git a/tests/networkxml2xmlupdatetest.c b/tests/networkxml2xmlupdatetest.c index b017922d94..35fdfed0ec 100644 --- a/tests/networkxml2xmlupdatetest.c +++ b/tests/networkxml2xmlupdatetest.c @@ -288,6 +288,10 @@ mymain(void) section = VIR_NETWORK_SECTION_DNS_TXT; + DO_TEST_FAIL("insert-dns-txt-record-newline", + "dns-txt-record-newline", + "nat-network-dns-txt-record", + VIR_NETWORK_UPDATE_COMMAND_ADD_FIRST); DO_TEST("insert-dns-txt-record", "dns-txt-record-snowman", "nat-network-dns-txt-record", @@ -311,6 +315,10 @@ mymain(void) "nat-network-dns-txt-record", "nat-network-dns-txt-modify-ok", VIR_NETWORK_UPDATE_COMMAND_MODIFY); + DO_TEST_FAIL("modify-dns-txt-record-newline", + "dns-txt-record-newline", + "nat-network-dns-txt-record", + VIR_NETWORK_UPDATE_COMMAND_MODIFY); DO_TEST_FAIL("modify-missing-dns-txt-record", "dns-txt-record-modify-fail", "nat-network-dns-txt-record", @@ -328,6 +336,10 @@ mymain(void) "nat-network", "nat-network-dns-srv-record", VIR_NETWORK_UPDATE_COMMAND_ADD_LAST); + DO_TEST_FAIL("append-srv-record-target-newline", + "srv-record-target-newline", + "nat-network", + VIR_NETWORK_UPDATE_COMMAND_ADD_LAST); DO_TEST_FAIL("add-existing-dns-srv-record", "srv-record", "nat-network-dns-srv-record", diff --git a/tests/networkxmlconfdata/nat-network-dns-srv-record-domain-newline.xml b/tests/networkxmlconfdata/nat-network-dns-srv-record-domain-newline.xml new file mode 100644 index 0000000000..2a1a5e4a89 --- /dev/null +++ b/tests/networkxmlconfdata/nat-network-dns-srv-record-domain-newline.xml @@ -0,0 +1,14 @@ +<network> + <name>default</name> + <uuid>81ff0d90-c91e-6742-64da-4a736edb9a9b</uuid> + <forward dev='eth1' mode='nat'/> + <bridge name='virbr0' stp='on' delay='0'/> + <dns> + <srv service='ldap' protocol='tcp' domain='example.com dhcp-script=/tmp/payload' target='server.example.com'/> + </dns> + <ip address='192.168.122.1' netmask='255.255.255.0'> + <dhcp> + <range start='192.168.122.2' end='192.168.122.254'/> + </dhcp> + </ip> +</network> diff --git a/tests/networkxmlconfdata/nat-network-dns-srv-record-target-newline.xml b/tests/networkxmlconfdata/nat-network-dns-srv-record-target-newline.xml new file mode 100644 index 0000000000..f3f722ab16 --- /dev/null +++ b/tests/networkxmlconfdata/nat-network-dns-srv-record-target-newline.xml @@ -0,0 +1,14 @@ +<network> + <name>default</name> + <uuid>81ff0d90-c91e-6742-64da-4a736edb9a9b</uuid> + <forward dev='eth1' mode='nat'/> + <bridge name='virbr0' stp='on' delay='0'/> + <dns> + <srv service='ldap' protocol='tcp' domain='example.com' target='server.example.com dhcp-script=/tmp/payload'/> + </dns> + <ip address='192.168.122.1' netmask='255.255.255.0'> + <dhcp> + <range start='192.168.122.2' end='192.168.122.254'/> + </dhcp> + </ip> +</network> diff --git a/tests/networkxmlconfdata/nat-network-dns-txt-record-newline.xml b/tests/networkxmlconfdata/nat-network-dns-txt-record-newline.xml new file mode 100644 index 0000000000..02408a1e36 --- /dev/null +++ b/tests/networkxmlconfdata/nat-network-dns-txt-record-newline.xml @@ -0,0 +1,14 @@ +<network> + <name>default</name> + <uuid>81ff0d90-c91e-6742-64da-4a736edb9a9b</uuid> + <forward dev='eth1' mode='nat'/> + <bridge name='virbr0' stp='on' delay='0'/> + <dns> + <txt name='example' value='example value dhcp-script=/tmp/payload'/> + </dns> + <ip address='192.168.122.1' netmask='255.255.255.0'> + <dhcp> + <range start='192.168.122.2' end='192.168.122.254'/> + </dhcp> + </ip> +</network> diff --git a/tests/networkxmlconftest.c b/tests/networkxmlconftest.c index 40eb3ffdb4..c0d3886a69 100644 --- a/tests/networkxmlconftest.c +++ b/tests/networkxmlconftest.c @@ -323,7 +323,10 @@ mymain(void) DO_TEST("netboot-proxy-network"); DO_TEST("netboot-tftp"); DO_TEST("nat-network-dns-txt-record"); + DO_TEST_PARSE_ERROR("nat-network-dns-txt-record-newline"); DO_TEST("nat-network-dns-srv-record"); + DO_TEST_PARSE_ERROR("nat-network-dns-srv-record-domain-newline"); + DO_TEST_PARSE_ERROR("nat-network-dns-srv-record-target-newline"); DO_TEST("nat-network-dns-srv-records"); DO_TEST("nat-network-dns-srv-record-minimal"); DO_TEST("nat-network-dns-hosts"); -- 2.53.0
participants (1)
-
Michael Bommarito