diff --git a/src/knot/nameserver/internet.c b/src/knot/nameserver/internet.c
index 034fd268c1c603325d536066299908a9e8e69be5..949642050a17352859d3a54f8cb34fbf2545ac3a 100644
--- a/src/knot/nameserver/internet.c
+++ b/src/knot/nameserver/internet.c
@@ -461,7 +461,7 @@ static knotd_in_state_t solve_name(knotd_in_state_t state, knot_pkt_t *pkt,
 {
 	int ret = zone_contents_find_dname(qdata->extra->contents, qdata->name,
 	                                   &qdata->extra->node, &qdata->extra->encloser,
-	                                   &qdata->extra->previous);
+	                                   &qdata->extra->previous, qdata->query->flags & KNOT_PF_NULLBYTE);
 
 	switch (ret) {
 	case ZONE_NAME_FOUND:
diff --git a/src/knot/nameserver/nsec_proofs.c b/src/knot/nameserver/nsec_proofs.c
index c99fd1fcbe38f876fbfcb5d08ec527c20e4a0bf6..9e7494645b64477128adf2e6d547a7a25775f807 100644
--- a/src/knot/nameserver/nsec_proofs.c
+++ b/src/knot/nameserver/nsec_proofs.c
@@ -190,7 +190,8 @@ static int put_covering_nsec(const zone_contents_t *zone,
 
 	const zone_node_t *proof = NULL;
 
-	int ret = zone_contents_find_dname(zone, name, &match, &closest, &prev);
+	int ret = zone_contents_find_dname(zone, name, &match, &closest, &prev,
+	                                   qdata->query->flags & KNOT_PF_NULLBYTE);
 	if (ret == ZONE_NAME_FOUND) {
 		proof = match;
 	} else if (ret == ZONE_NAME_NOT_FOUND) {
diff --git a/src/knot/zone/contents.c b/src/knot/zone/contents.c
index e0f4e581c928def0beae61ed0ce713f66e072b75..760a0c5f81c68e139334ea24c6fb52d472d749e2 100644
--- a/src/knot/zone/contents.c
+++ b/src/knot/zone/contents.c
@@ -280,7 +280,8 @@ int zone_contents_find_dname(const zone_contents_t *zone,
                              const knot_dname_t *name,
                              const zone_node_t **match,
                              const zone_node_t **closest,
-                             const zone_node_t **previous)
+                             const zone_node_t **previous,
+                             bool name_nullbyte)
 {
 	if (name == NULL || match == NULL || closest == NULL) {
 		return KNOT_EINVAL;
@@ -306,6 +307,15 @@ int zone_contents_find_dname(const zone_contents_t *zone,
 		// if previous==NULL, zone not adjusted yet
 
 		assert(node);
+
+		// WARNING: for the sake of efficiency, Knot does not handle \0 byte in qname correctly
+		// which can lead to disasters here and there. This should cover most of the cases.
+		bool node_nullbyte = (node->flags & NODE_FLAGS_NULLBYTE);
+		if (node_nullbyte != name_nullbyte ||
+		    (node_nullbyte && !knot_dname_is_equal(node->owner, name))) {
+			goto nxd;
+		}
+
 		*match = node;
 		*closest = node;
 		if (previous != NULL) {
@@ -318,7 +328,7 @@ int zone_contents_find_dname(const zone_contents_t *zone,
 		// closest match
 
 		assert(!node && prev);
-
+nxd:
 		node = prev;
 		size_t matched_labels = knot_dname_matched_labels(node->owner, name);
 		while (matched_labels < knot_dname_labels(node->owner, NULL)) {
@@ -431,7 +441,7 @@ bool zone_contents_find_node_or_wildcard(const zone_contents_t *contents,
                                          const zone_node_t **found)
 {
 	const zone_node_t *encloser = NULL;
-	zone_contents_find_dname(contents, find, found, &encloser, NULL);
+	zone_contents_find_dname(contents, find, found, &encloser, NULL, knot_dname_with_null(find));
 	if (*found == NULL && encloser != NULL && (encloser->flags & NODE_FLAGS_WILDCARD_CHILD)) {
 		*found = zone_contents_find_wildcard_child(contents, encloser);
 		assert(*found != NULL);
diff --git a/src/knot/zone/contents.h b/src/knot/zone/contents.h
index e92bf61d427772d28fee741204fa9e2ca6999e86..0acde831959ed82c59bc86ebda8fa234f9ec699b 100644
--- a/src/knot/zone/contents.h
+++ b/src/knot/zone/contents.h
@@ -119,6 +119,7 @@ zone_node_t *zone_contents_find_node_for_rr(zone_contents_t *contents, const kno
  *                       May match \a match if found exactly.
  * \param[out] previous  Previous domain name in canonical order.
  *                       Always previous, won't match \a match.
+ * \param[in] name_nullbyte The \a name parameter contains \0 byte.
  *
  * \note The encloser and previous mustn't be used directly for DNSSEC proofs.
  *       These nodes may be empty non-terminals or not authoritative.
@@ -133,7 +134,8 @@ int zone_contents_find_dname(const zone_contents_t *contents,
                              const knot_dname_t *name,
                              const zone_node_t **match,
                              const zone_node_t **closest,
-                             const zone_node_t **previous);
+                             const zone_node_t **previous,
+                             bool name_nullbyte);
 
 /*!
  * \brief Tries to find a node with the specified name among the NSEC3 nodes
diff --git a/src/knot/zone/node.c b/src/knot/zone/node.c
index 291454bd7693e2d27c6deb4c41f6147ab0917de1..82d930d8f62792345410430176ecfb02ed9bf193 100644
--- a/src/knot/zone/node.c
+++ b/src/knot/zone/node.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -124,6 +124,10 @@ zone_node_t *node_new(const knot_dname_t *owner, bool binode, bool second, knot_
 	// Node is authoritative by default.
 	ret->flags = NODE_FLAGS_AUTH;
 
+	if (knot_dname_with_null(owner)) {
+		ret->flags |= NODE_FLAGS_NULLBYTE;
+	}
+
 	if (binode) {
 		ret->flags |= NODE_FLAGS_BINODE;
 		if (second) {
diff --git a/src/knot/zone/node.h b/src/knot/zone/node.h
index d30cc6e1c401d731b75ca5805662efebea7ef8c8..cab0604f96f9001c4093d9957a23a197ee793218 100644
--- a/src/knot/zone/node.h
+++ b/src/knot/zone/node.h
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -104,6 +104,8 @@ enum node_flags {
 	NODE_FLAGS_SUBTREE_AUTH =    1 << 11,
 	/*! \brief The node or some node in subtree has any data in it, possibly just insec deleg. */
 	NODE_FLAGS_SUBTREE_DATA =    1 << 12,
+	/*! \brief Node owner name contains \0 byte in some label. */
+	NODE_FLAGS_NULLBYTE =        1 << 13,
 };
 
 typedef void (*node_addrem_cb)(zone_node_t *, void *);
diff --git a/src/knot/zone/semantic-check.c b/src/knot/zone/semantic-check.c
index 3d085d875fa2b8c57dbd7ba13721d5ef3fdeac82..d449c5f7782604fcc753cfd5587e6a885196051f 100644
--- a/src/knot/zone/semantic-check.c
+++ b/src/knot/zone/semantic-check.c
@@ -173,7 +173,8 @@ static int check_delegation(const zone_node_t *node, semchecks_data_t *data)
 		const knot_dname_t *ns_dname = knot_ns_name(ns_rr);
 		const zone_node_t *glue_node = NULL, *glue_encloser = NULL;
 		int ret = zone_contents_find_dname(data->zone, ns_dname, &glue_node,
-		                                   &glue_encloser, NULL);
+		                                   &glue_encloser, NULL,
+		                                   knot_dname_with_null(ns_dname));
 		switch (ret) {
 		case KNOT_EOUTOFZONE:
 			continue; // NS is out of bailiwick
diff --git a/src/libknot/packet/pkt.c b/src/libknot/packet/pkt.c
index 728bb3ee25fead14ebe130b1c8cb5584122a6bbe..94bb310db8027bcdfa692edba81d2f2356a17330 100644
--- a/src/libknot/packet/pkt.c
+++ b/src/libknot/packet/pkt.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -616,6 +616,11 @@ int knot_pkt_parse_question(knot_pkt_t *pkt)
 	/* Copy QNAME and canonicalize to lowercase. */
 	knot_dname_copy_lower(pkt->lower_qname, pkt->wire + KNOT_WIRE_HEADER_SIZE);
 
+	size_t str_len = strnlen((char *)(pkt->wire + KNOT_WIRE_HEADER_SIZE), pkt->qname_size) + 1;
+	if (pkt->qname_size != str_len) {
+		pkt->flags |= KNOT_PF_NULLBYTE;
+	}
+
 	return KNOT_EOK;
 }
 
diff --git a/src/libknot/packet/pkt.h b/src/libknot/packet/pkt.h
index da69c8cfcb6a6f3020fca093c0640092ee5bb8e0..ac7ac18515defe14beb12ae723546350e270f76c 100644
--- a/src/libknot/packet/pkt.h
+++ b/src/libknot/packet/pkt.h
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -52,6 +52,7 @@ enum {
 	KNOT_PF_NOCANON   = 1 << 5, /*!< Don't canonicalize rrsets during parsing. */
 	KNOT_PF_ORIGTTL   = 1 << 6, /*!< Write RRSIGs with their original TTL. */
 	KNOT_PF_SOAMINTTL = 1 << 7, /*!< Write SOA with its minimum-ttl as TTL. */
+	KNOT_PF_NULLBYTE  = 1 << 8, /*!< At lest one \0 byte is present in some qname label. */
 };
 
 typedef struct knot_pkt knot_pkt_t;
diff --git a/tests-extra/tests/basic/zerobyte/data/test.zone b/tests-extra/tests/basic/zerobyte/data/test.zone
new file mode 100644
index 0000000000000000000000000000000000000000..01c5592dc3f665877dd5dab05ee9e614ab42383d
--- /dev/null
+++ b/tests-extra/tests/basic/zerobyte/data/test.zone
@@ -0,0 +1,9 @@
+$ORIGIN test.
+
+@	SOA	dns hostmaster 2010111201 10800 3600 1209600 7200
+	NS	dns
+dns	A	192.0.2.1
+
+psy	TXT	text
+cho.psy	NS	example.com.
+exis\000ing A	1.2.3.4
diff --git a/tests-extra/tests/basic/zerobyte/test.py b/tests-extra/tests/basic/zerobyte/test.py
new file mode 100644
index 0000000000000000000000000000000000000000..9dc7361788d207250f84e8b08748d9634bd0f5ce
--- /dev/null
+++ b/tests-extra/tests/basic/zerobyte/test.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+
+'''Test for zero byte in a QNAME label.'''
+
+from dnstest.test import Test
+
+t = Test()
+
+master = t.server("knot")
+zone = t.zone("test.", storage=".")
+
+t.link(zone, master)
+
+master.dnssec(zone).enable = True
+master.dnssec(zone).nsec3 = True
+master.dnssec(zone).nsec3_opt_out = True
+
+t.start()
+
+master.zone_wait(zone)
+
+resp = master.dig("psy\\000cho.test.", "A", dnssec=True)
+resp.check(rcode="NXDOMAIN")
+
+resp = master.dig("psy\\000cho\\000nxd.test.", "A", dnssec=True)
+resp.check(rcode="NXDOMAIN")
+
+resp = master.dig("exis\\000ing.test.", "A", dnssec=True)
+resp.check(rcode="NOERROR", rdata="1.2.3.4")
+
+resp = master.dig("ing.exis.test.", "A", dnssec=True)
+resp.check(rcode="NXDOMAIN", nordata="1.2.3.4")
+
+t.end()