]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
netfilter: xtables: fix L4 header parsing for non-first fragments
authorFernando Fernandez Mancera <fmancera@suse.de>
Tue, 28 Apr 2026 10:25:48 +0000 (12:25 +0200)
committerPablo Neira Ayuso <pablo@netfilter.org>
Thu, 30 Apr 2026 15:59:01 +0000 (17:59 +0200)
Multiple targets and matches relies on L4 header to operate. For
fragmented packets, every fragment carries the transport protocol
identifier, but only the first fragment contains the L4 header.

As the 'raw' table can be configured to run at priority -450 (before
defragmentation at -400), the target/match can be reached before
reassembly. In this case, non-first fragments have their payload
incorrectly parsed as a TCP/UDP header. This would be of course a
misconfiguration scenario. In most of the cases this just lead to a
unreliable behavior for fragmented traffic.

Add a fragment check to ensure target/match only evaluates unfragmented
packets or the first fragment in the stream.

Fixes: 902d6a4c2a4f ("netfilter: nf_defrag: Skip defrag if NOTRACK is set")
Signed-off-by: Fernando Fernandez Mancera <fmancera@suse.de>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
net/netfilter/xt_TPROXY.c
net/netfilter/xt_ecn.c
net/netfilter/xt_hashlimit.c
net/netfilter/xt_osf.c
net/netfilter/xt_tcpmss.c

index e4bea1d346cf923d91d8be4b7d55d41388fcf7fb..5f60e7298a1ea9894c55021735816b76b80fea34 100644 (file)
@@ -86,6 +86,9 @@ tproxy_tg4_v0(struct sk_buff *skb, const struct xt_action_param *par)
 {
        const struct xt_tproxy_target_info *tgi = par->targinfo;
 
+       if (par->fragoff)
+               return NF_DROP;
+
        return tproxy_tg4(xt_net(par), skb, tgi->laddr, tgi->lport,
                          tgi->mark_mask, tgi->mark_value);
 }
@@ -95,6 +98,9 @@ tproxy_tg4_v1(struct sk_buff *skb, const struct xt_action_param *par)
 {
        const struct xt_tproxy_target_info_v1 *tgi = par->targinfo;
 
+       if (par->fragoff)
+               return NF_DROP;
+
        return tproxy_tg4(xt_net(par), skb, tgi->laddr.ip, tgi->lport,
                          tgi->mark_mask, tgi->mark_value);
 }
@@ -106,6 +112,7 @@ tproxy_tg6_v1(struct sk_buff *skb, const struct xt_action_param *par)
 {
        const struct ipv6hdr *iph = ipv6_hdr(skb);
        const struct xt_tproxy_target_info_v1 *tgi = par->targinfo;
+       unsigned short fragoff = 0;
        struct udphdr _hdr, *hp;
        struct sock *sk;
        const struct in6_addr *laddr;
@@ -113,8 +120,8 @@ tproxy_tg6_v1(struct sk_buff *skb, const struct xt_action_param *par)
        int thoff = 0;
        int tproto;
 
-       tproto = ipv6_find_hdr(skb, &thoff, -1, NULL, NULL);
-       if (tproto < 0)
+       tproto = ipv6_find_hdr(skb, &thoff, -1, &fragoff, NULL);
+       if (tproto < 0 || fragoff)
                return NF_DROP;
 
        hp = skb_header_pointer(skb, thoff, sizeof(_hdr), &_hdr);
index b96e8203ac54982e562ea083f390eff5d213477e..a8503f5d26bf41856c1fc9f5434d6eed1e5b7824 100644 (file)
@@ -30,6 +30,10 @@ static bool match_tcp(const struct sk_buff *skb, struct xt_action_param *par)
        struct tcphdr _tcph;
        const struct tcphdr *th;
 
+       /* this is fine for IPv6 as ecn_mt_check6() enforces -p tcp */
+       if (par->fragoff)
+               return false;
+
        /* In practice, TCP match does this, so can't fail.  But let's
         * be good citizens.
         */
index 3bd127bfc11494923c7f84a2c9a93dbf2cfaa836..2704b4b60d1e04a11fc556d6d2f5090d22b9dd6b 100644 (file)
@@ -658,6 +658,8 @@ hashlimit_init_dst(const struct xt_hashlimit_htable *hinfo,
                if (!(hinfo->cfg.mode &
                      (XT_HASHLIMIT_HASH_DPT | XT_HASHLIMIT_HASH_SPT)))
                        return 0;
+               if (ntohs(ip_hdr(skb)->frag_off) & IP_OFFSET)
+                       return -1;
                nexthdr = ip_hdr(skb)->protocol;
                break;
 #if IS_ENABLED(CONFIG_IP6_NF_IPTABLES)
@@ -681,7 +683,7 @@ hashlimit_init_dst(const struct xt_hashlimit_htable *hinfo,
                        return 0;
                nexthdr = ipv6_hdr(skb)->nexthdr;
                protoff = ipv6_skip_exthdr(skb, sizeof(struct ipv6hdr), &nexthdr, &frag_off);
-               if ((int)protoff < 0)
+               if ((int)protoff < 0 || ntohs(frag_off) & IP6_OFFSET)
                        return -1;
                break;
        }
index dc9485854002aa7d79f69c1d1ce0dbb95b88dc8a..e8807caede68e63792308888de32df58dba412b8 100644 (file)
@@ -27,6 +27,9 @@
 static bool
 xt_osf_match_packet(const struct sk_buff *skb, struct xt_action_param *p)
 {
+       if (p->fragoff)
+               return false;
+
        return nf_osf_match(skb, xt_family(p), xt_hooknum(p), xt_in(p),
                            xt_out(p), p->matchinfo, xt_net(p), nf_osf_fingers);
 }
index 0d32d4841cb325efd09ed5d0bf1772e6c9bd1eeb..b9da8269161d8f77ee385bc3d7b60e0761b70d22 100644 (file)
@@ -32,6 +32,10 @@ tcpmss_mt(const struct sk_buff *skb, struct xt_action_param *par)
        u8 _opt[15 * 4 - sizeof(_tcph)];
        unsigned int i, optlen;
 
+       /* this is fine for IPv6 as xt_tcpmss enforces -p tcp */
+       if (par->fragoff)
+               return false;
+
        /* If we don't have the whole header, drop packet. */
        th = skb_header_pointer(skb, par->thoff, sizeof(_tcph), &_tcph);
        if (th == NULL)