diff --git docs/settings.rst docs/settings.rst index fbe3da919..bcb98d939 100644 --- docs/settings.rst +++ docs/settings.rst @@ -1297,10 +1297,55 @@ Limit the number of NSEC3 hash iterations for zone configurations. For more information see :ref:`dnssec-operational-nsec-modes-params`. .. note:: Pre 4.5.0 the default was 500. +.. _setting-max-pointers-per-name: + +``max-pointers-per-name`` +------------------------- + +- Integer +- Default: 10 + +Maximum number of name pointers followed per name. +DNS packets needing more than the limit will fail to parse and be rejected. + +.. versionadded:: 5.1.5 +.. versionadded:: 5.0.8 +.. versionadded:: 4.9.18 + +.. _setting-max-pointers-per-packet-byte: + +``max-pointers-per-packet-byte`` +-------------------------------- + +- Integer +- Default: 4 + +Maximum number of name pointers per DNS packet byte followed. +DNS packets needing more than the limit will fail to parse and be rejected. + +.. versionadded:: 5.1.5 +.. versionadded:: 5.0.8 +.. versionadded:: 4.9.18 + +.. _setting-max-pointers-to-pointers: + +``max-pointers-to-pointers`` +---------------------------- + +- Integer +- Default: 4 + +Maximum number of name pointers pointing to directly another pointer per name. +DNS packets needing more than the limit will fail to parse and be rejected. + +.. versionadded:: 5.1.5 +.. versionadded:: 5.0.8 +.. versionadded:: 4.9.18 + .. _setting-max-packet-cache-entries: ``max-packet-cache-entries`` ---------------------------- diff --git pdns/auth-main.cc pdns/auth-main.cc index 66e819d89..536d3db49 100644 --- pdns/auth-main.cc +++ pdns/auth-main.cc @@ -363,10 +363,14 @@ static void declareArguments() // explicitly declared outside of HAVE_LUA_RECORDS guard to prevent writes // even when LUA is not currently used ::arg().setSwitch("enable-lua-record-updates", "Allow updates to Lua records") = "no"; + ::arg().set("max-pointers-per-packet-byte", "Maximum number of name pointers per DNS packet byte followed") = "4"; + ::arg().set("max-pointers-to-pointers", "Maximum number of name pointers pointing directly to another pointer per name") = "4"; + ::arg().set("max-pointers-per-name", "Maximum number of name pointers followed per name") = "10"; + // FIXME520: remove when branching 5.2 ::arg().set("entropy-source", "") = ""; ::arg().set("rng", "") = ""; ::arg().setDefaults(); @@ -851,10 +855,13 @@ static void mainthread() PacketHandler::s_SVCAutohints = ::arg().mustDo("svc-autohints"); PacketHandler::s_NAPTRprocessing = ::arg().mustDo("naptr-additional-processing"); g_proxyProtocolACL.toMasks(::arg()["proxy-protocol-from"]); g_proxyProtocolMaximumSize = ::arg().asNum("proxy-protocol-maximum-size"); + PacketReader::s_MaxPointersPerByte = ::arg().asNum("max-pointers-per-packet-byte"); + DNSName::s_maxPointersToPointers = ::arg().asNum("max-pointers-to-pointers"); + DNSName::s_maxPointersPerName = ::arg().asNum("max-pointers-per-name"); if (::arg()["edns-cookie-secret"].size() != 0) { // User wants cookie processing #ifdef HAVE_CRYPTO_SHORTHASH // we can do siphash-based cookies DNSPacket::s_doEDNSCookieProcessing = true; diff --git pdns/dnsname.cc pdns/dnsname.cc index b4fb679ab..79fd578dc 100644 --- pdns/dnsname.cc +++ pdns/dnsname.cc @@ -45,10 +45,13 @@ const DNSName g_hmacsha256dnsname("hmac-sha256"); const DNSName g_hmacsha384dnsname("hmac-sha384"); const DNSName g_hmacsha512dnsname("hmac-sha512"); const ZoneName g_rootzonename("."); +uint16_t DNSName::s_maxPointersToPointers = 4; +uint16_t DNSName::s_maxPointersPerName = 10; + /* raw storage in DNS label format, with trailing 0. W/o trailing 0, we are 'empty' www.powerdns.com = 3www8powerdns3com0 */ @@ -113,22 +116,22 @@ DNSName::DNSName(const std::string_view sw) } } } } -DNSName::DNSName(const char* pos, size_t len, size_t offset, bool uncompress, uint16_t* qtype, uint16_t* qclass, unsigned int* consumed, uint16_t minOffset) +DNSName::DNSName(const char* pos, size_t len, size_t offset, bool uncompress, uint16_t* qtype, uint16_t* qclass, unsigned int* consumed, uint16_t minOffset, size_t* pointerBudget) { if (offset >= len) throw std::range_error("Trying to read past the end of the buffer ("+std::to_string(offset)+ " >= "+std::to_string(len)+")"); if(!uncompress) { if(const void * fnd=memchr(pos+offset, 0, len-offset)) { d_storage.reserve(2+(const char*)fnd-(pos+offset)); } } - packetParser(pos, len, offset, uncompress, qtype, qclass, consumed, 0, minOffset); + packetParser(pos, len, offset, uncompress, qtype, qclass, consumed, 0, minOffset, 0, pointerBudget); } static void checkLabelLength(uint8_t length) { if (length == 0) { @@ -197,11 +200,11 @@ size_t DNSName::parsePacketUncompressed(const pdns::views::UnsignedCharView& vie } return pos; } // this should be the __only__ dns name parser in PowerDNS. -void DNSName::packetParser(const char* qpos, size_t len, size_t offset, bool uncompress, uint16_t* qtype, uint16_t* qclass, unsigned int* consumed, int depth, uint16_t minOffset) +void DNSName::packetParser(const char* qpos, size_t len, size_t offset, bool uncompress, uint16_t* qtype, uint16_t* qclass, unsigned int* consumed, int depth, uint16_t minOffset, uint16_t pointerChainLength, size_t *pointerBudget) { if (offset >= len) { throw std::range_error("Trying to read past the end of the buffer ("+std::to_string(offset)+ " >= "+std::to_string(len)+")"); } @@ -211,11 +214,28 @@ void DNSName::packetParser(const char* qpos, size_t len, size_t offset, bool unc unsigned char labellen{0}; pdns::views::UnsignedCharView view(qpos, len); auto pos = parsePacketUncompressed(view, offset, uncompress); + labellen = view.at(pos); + if (pos == offset && labellen != 0) { + // It's a pointer + if (pointerBudget != nullptr) { + if (*pointerBudget == 0) { + throw std::range_error("Too many compression pointers processed in packet"); + } + (*pointerBudget)--; + } + // Are we chasing a pointer chain already for this name? + if (pointerChainLength++ > s_maxPointersToPointers) { + throw std::range_error("Too many pointer to pointers during decompression (" + std::to_string(pointerChainLength) + ")"); + } + } + else { + pointerChainLength = 0; // not an immediate pointer to pointer case any more + } pos++; if (labellen != 0 && pos < view.size()) { if (labellen < 0xc0) { abort(); } @@ -233,15 +253,15 @@ void DNSName::packetParser(const char* qpos, size_t len, size_t offset, bool unc if (newpos < minOffset) { throw std::range_error("Invalid label position during decompression ("+std::to_string(newpos)+ " < "+std::to_string(minOffset)+")"); } - if (++depth > 100) { - throw std::range_error("Abort label decompression after 100 redirects"); + if (++depth > s_maxPointersPerName) { + throw std::range_error("Abort label decompression after too many redirects (" + std::to_string(depth) + ")"); } - packetParser(qpos, len, newpos, true, nullptr, nullptr, nullptr, depth, minOffset); + packetParser(qpos, len, newpos, true, nullptr, nullptr, nullptr, depth, minOffset, pointerChainLength, pointerBudget); pos++; } if (d_storage.empty()) { diff --git pdns/dnsname.hh pdns/dnsname.hh index 2dd3cec65..7c45d6fac 100644 --- pdns/dnsname.hh +++ pdns/dnsname.hh @@ -86,11 +86,14 @@ inline unsigned char dns_tolower(unsigned char chr) class ZoneName; #endif class DNSName { public: - static const size_t s_maxDNSNameLength = 255; + static constexpr size_t s_maxDNSNameLength = 255; + static constexpr size_t s_maxDNSLabelLength = 63; + static uint16_t s_maxPointersToPointers; + static uint16_t s_maxPointersPerName; DNSName() = default; //!< Constructs an *empty* DNSName, NOT the root! // Work around assertion in some boost versions that do not like self-assignment of boost::container::string DNSName& operator=(const DNSName& rhs) { @@ -108,11 +111,11 @@ public: } DNSName(const DNSName& a) = default; DNSName(DNSName&& a) = default; explicit DNSName(std::string_view sw); //!< Constructs from a human formatted, escaped presentation - DNSName(const char* p, size_t len, size_t offset, bool uncompress, uint16_t* qtype = nullptr, uint16_t* qclass = nullptr, unsigned int* consumed = nullptr, uint16_t minOffset = 0); //!< Construct from a DNS Packet, taking the first question if offset=12. If supplied, consumed is set to the number of bytes consumed from the packet, which will not be equal to the wire length of the resulting name in case of compression. + DNSName(const char* pos, size_t len, size_t offset, bool uncompress, uint16_t* qtype = nullptr, uint16_t* qclass = nullptr, unsigned int* consumed = nullptr, uint16_t minOffset = 0, size_t* pointerBudget = nullptr); //!< Construct from a DNS Packet, taking the first question if offset=12. If supplied, consumed is set to the number of bytes consumed from the packet, which will not be equal to the wire length of the resulting name in case of compression. bool isPartOf(const DNSName& rhs) const; //!< Are we part of the rhs name? Note that name.isPartOf(name). inline bool operator==(const DNSName& rhs) const; //!< DNS-native comparison (case insensitive) - empty compares to empty bool operator!=(const DNSName& other) const { return !(*this == other); } // !< DNS-native (case insensitive) comparison against raw data in (uncompressed) wire format. The view has to start with the DNS name, but does not have to contain only a DNS name. Roughly, passing a view of a DNS packet starting just after the DNS header is OK, everything else is not because any names present later in the packet might be compressed. @@ -245,11 +248,11 @@ public: #endif // ] private: string_t d_storage; - void packetParser(const char* qpos, size_t len, size_t offset, bool uncompress, uint16_t* qtype, uint16_t* qclass, unsigned int* consumed, int depth, uint16_t minOffset); + void packetParser(const char* qpos, size_t len, size_t offset, bool uncompress, uint16_t* qtype, uint16_t* qclass, unsigned int* consumed, int depth, uint16_t minOffset, uint16_t pointerChainLength, size_t* pointerBudget); size_t parsePacketUncompressed(const pdns::views::UnsignedCharView& view, size_t position, bool uncompress); static void appendEscapedLabel(std::string& appendTo, const char* orig, size_t len); static std::string unescapeLabel(const std::string& orig); static void throwSafeRangeError(const std::string& msg, const char* buf, size_t length); }; diff --git pdns/dnsparser.cc pdns/dnsparser.cc index da0ea5568..8bcef5e18 100644 --- pdns/dnsparser.cc +++ pdns/dnsparser.cc @@ -232,11 +232,11 @@ DNSResourceRecord DNSResourceRecord::fromWire(const DNSRecord& wire) resourceRecord.auth = false; resourceRecord.qclass = wire.d_class; return resourceRecord; } -void MOADNSParser::init(bool query, const std::string_view& packet) +void MOADNSParser::init(bool query, const std::string_view& packet, size_t pointerBudget) { if (packet.size() < sizeof(dnsheader)) throw MOADNSException("Packet shorter than minimal header"); memcpy(&d_header, packet.data(), sizeof(dnsheader)); @@ -253,10 +253,13 @@ void MOADNSParser::init(bool query, const std::string_view& packet) throw MOADNSException("Query with QD > 1 ("+std::to_string(d_header.qdcount)+")"); unsigned int n=0; PacketReader pr(packet); + if (pointerBudget != 0) { + pr.setPointerBudget(pointerBudget); + } bool validPacket=false; try { d_qtype = d_qclass = 0; // sometimes replies come in with no question, don't present garbage then for(n=0;n < d_header.qdcount; ++n) { @@ -365,10 +368,12 @@ bool MOADNSParser::hasEDNS() const } return false; } +size_t PacketReader::s_MaxPointersPerByte = 4; + void PacketReader::getDnsrecordheader(struct dnsrecordheader &ah) { unsigned char *p = reinterpret_cast(&ah); for(unsigned int n = 0; n < sizeof(dnsrecordheader); ++n) { @@ -469,16 +474,16 @@ uint8_t PacketReader::get8BitInt() return d_content.at(d_pos++); } DNSName PacketReader::getName() { - unsigned int consumed; + unsigned int consumed{}; try { - DNSName dn((const char*) d_content.data(), d_content.size(), d_pos, true /* uncompress */, nullptr /* qtype */, nullptr /* qclass */, &consumed, sizeof(dnsheader)); + DNSName name((const char*) d_content.data(), d_content.size(), d_pos, true /* uncompress */, nullptr /* qtype */, nullptr /* qclass */, &consumed, sizeof(dnsheader), &d_pointerBudget); - d_pos+=consumed; - return dn; + d_pos += consumed; + return name; } catch(const std::range_error& re) { throw std::out_of_range(string("dnsname issue: ")+re.what()); } catch(...) { diff --git pdns/dnsparser.hh pdns/dnsparser.hh index 6c9dc3b2e..b00c439d0 100644 --- pdns/dnsparser.hh +++ pdns/dnsparser.hh @@ -69,15 +69,23 @@ class PacketReader { public: PacketReader(const std::string_view& content, uint16_t initialPos=sizeof(dnsheader), bool internalRepresentation = false) : d_pos(initialPos), d_startrecordpos(initialPos), d_content(content), d_internal(internalRepresentation) { - if(content.size() > std::numeric_limits::max()) + if (content.size() > std::numeric_limits::max()) { throw std::out_of_range("packet too large"); + } - d_recordlen = (uint16_t) content.size(); + d_recordlen = static_cast(content.size()); not_used = 0; + d_pointerBudget = static_cast(d_recordlen) * s_MaxPointersPerByte; + } + + // Default is s_MaxPointersPerByte * packet size + void setPointerBudget(size_t maxPointers) + { + d_pointerBudget = maxPointers; } uint32_t get32BitInt(); uint16_t get16BitInt(); uint8_t get8BitInt(); @@ -203,11 +211,14 @@ public: throw std::out_of_range("Attempt to skip bytes (" + std::to_string(n) + " starting at " + std::to_string(d_pos) + ") farther than the packet's end (" + std::to_string(d_content.size()) + ")"); } d_pos += n; } + static size_t s_MaxPointersPerByte; + private: + size_t d_pointerBudget{0}; uint16_t d_pos; uint16_t d_startrecordpos; // needed for getBlob later on uint16_t d_recordlen; // ditto uint16_t not_used; // Aligns the whole class on 8-byte boundaries const std::string_view d_content; @@ -527,13 +538,13 @@ public: { init(query, buffer); } //! Parse from a pointer and length - MOADNSParser(bool query, const char *packet, unsigned int len) : d_tsigPos(0) + MOADNSParser(bool query, const char *packet, unsigned int len, size_t pointerBudget = 0) : d_tsigPos(0) { - init(query, std::string_view(packet, len)); + init(query, std::string_view(packet, len), pointerBudget); } DNSName d_qname; uint16_t d_qclass, d_qtype; dnsheader d_header; @@ -549,11 +560,11 @@ public: } bool hasEDNS() const; private: - void init(bool query, const std::string_view& packet); + void init(bool query, const std::string_view& packet, size_t pointerBudget = 0); uint16_t d_tsigPos; }; string simpleCompress(const string& label, const string& root=""); void shuffleDNSPacket(char* packet, size_t length, const dnsheader_aligned& aligned_dh);