diff --git docs/settings.rst docs/settings.rst index 482ec473e..dcad56823 100644 --- docs/settings.rst +++ docs/settings.rst @@ -1250,10 +1250,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 307a3d504..94b7da9e8 100644 --- pdns/auth-main.cc +++ pdns/auth-main.cc @@ -346,10 +346,14 @@ static void declareArguments() ::arg().set("gss-max-contexts", "The maximum number of simultaneous GSS contexts allowed") = "1000"; #endif ::arg().setSwitch("views", "Enable views (variants) of zones, for backends which support them") = "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"; + ::arg().setDefaults(); } static time_t s_start = time(nullptr); static uint64_t uptimeOfProcess(const std::string& /* str */) @@ -739,10 +743,13 @@ static void mainthread() DNSPacket::s_doEDNSSubnetProcessing = ::arg().mustDo("edns-subnet-processing"); PacketHandler::s_SVCAutohints = ::arg().mustDo("svc-autohints"); 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 6c328857e..afc2cc949 100644 --- pdns/dnsname.cc +++ pdns/dnsname.cc @@ -30,10 +30,13 @@ #include const DNSName g_rootdnsname("."), g_wildcarddnsname("*"); 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 */ @@ -98,22 +101,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) { @@ -182,11 +185,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)+")"); } @@ -196,11 +199,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(); } @@ -218,15 +238,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 b498b5e8a..ca383677a 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); } @@ -242,11 +245,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 8d9e2968d..beaef9883 100644 --- pdns/dnsparser.cc +++ pdns/dnsparser.cc @@ -225,11 +225,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)); @@ -246,10 +246,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) { @@ -351,10 +354,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) { @@ -452,16 +457,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 7bd667dee..c5f95da9a 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(); @@ -178,11 +186,14 @@ public: void skip(uint16_t n) { 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; @@ -499,13 +510,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; @@ -521,11 +532,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 ageDNSPacket(char* packet, size_t length, uint32_t seconds, const dnsheader_aligned&);