From: Miod Vallat Date: Mon, 22 Sep 2025 08:42:47 +0000 (+0200) Subject: Perform character set validation of view names. X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F16152%2Fhead;p=thirdparty%2Fpdns.git Perform character set validation of view names. Signed-off-by: Miod Vallat --- diff --git a/docs/views.rst b/docs/views.rst index f28ddba88..3fb83ef5f 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -85,6 +85,10 @@ that view, as their variantless contents. Only one variant per zone may appear in a view; setting a new zone variant will replace the previous one in the view. +View names are case-sensitive and may be composed of letters, digits, spaces, +as well as `-` (dash), `.` (dot) and `_` (underscore). They are not allowed to +start with a dot or a space. + Resolution Algorithm -------------------- diff --git a/pdns/check-zone.cc b/pdns/check-zone.cc index d26f5a404..431da983e 100644 --- a/pdns/check-zone.cc +++ b/pdns/check-zone.cc @@ -28,6 +28,31 @@ namespace Check { +bool validateViewName(std::string_view name, std::string& error) +{ + if (name.empty()) { + error = "Empty view names are not allowed"; + return false; + } + + if (auto pos = name.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 _-."); pos != std::string_view::npos) { + error = std::string("View name contains forbidden character '") + name[pos] + "' at position " + std::to_string(pos); + return false; + } + + if (name[0] == '.') { + error = "View names are not allowed to start with a dot"; + return false; + } + + if (name[0] == ' ') { + error = "View names are not allowed to start with a space"; + return false; + } + + return true; +} + void checkRRSet(const vector& oldrrs, vector& allrrs, const ZoneName& zone, vector>& errors) { // QTypes that MUST NOT have multiple records of the same type in a given RRset. diff --git a/pdns/check-zone.hh b/pdns/check-zone.hh index 89b4577c5..6d43f09b7 100644 --- a/pdns/check-zone.hh +++ b/pdns/check-zone.hh @@ -20,9 +20,20 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +// These validation/verification routines are used by both pdnsutil and +// the pdns_server REST API. +// They build error messages, if any, into an object provided by the caller +// (preferrably a container if it makes sense to report multiple errors); +// it's up to each caller to decide how to report such errors. + namespace Check { +// Validate a view name. Although view names never appear on the wire, we +// restrict them to [a-zA-Z0-9-_. ], with empty names or names with leading +// whitespace or a leading dot forbidden. +bool validateViewName(std::string_view name, std::string& error); + // Returns the list of errors found for new records which violate RRset // constraints. // NOTE: sorts records in-place. diff --git a/pdns/pdnsutil.cc b/pdns/pdnsutil.cc index cd3f47aa8..06aa5322e 100644 --- a/pdns/pdnsutil.cc +++ b/pdns/pdnsutil.cc @@ -5440,6 +5440,11 @@ static int listView(vector& cmds, const std::string_view synopsis) UtilBackend B("default"); //NOLINT(readability-identifier-length) + if ((B.getCapabilities() & DNSBackend::CAP_VIEWS) == 0) { + cerr << "None of the configured backends support views." << endl; + return 1; + } + vector ret; B.viewListZones(cmds.at(0), ret); @@ -5457,6 +5462,12 @@ static int listViews(vector& cmds, const std::string_view synopsis) UtilBackend B("default"); //NOLINT(readability-identifier-length) + if ((B.getCapabilities() & DNSBackend::CAP_VIEWS) == 0) { + // Don't complain about the lack of view support in this case, but + // don't list anything either. + return 0; + } + vector ret; B.viewList(ret); @@ -5474,7 +5485,17 @@ static int viewAddZone(vector& cmds, const std::string_view synopsis) UtilBackend B("default"); //NOLINT(readability-identifier-length) + if ((B.getCapabilities() & DNSBackend::CAP_VIEWS) == 0) { + cerr << "None of the configured backends support views." << endl; + return 1; + } + string view{cmds.at(0)}; + string error; + if (!Check::validateViewName(view, error)) { + cerr << error << "." << endl; + return 1; + } ZoneName zone{cmds.at(1)}; if (!B.viewAddZone(view, zone)) { cerr<<"Operation failed."<& cmds, const std::string_view synopsis) UtilBackend B("default"); //NOLINT(readability-identifier-length) + if ((B.getCapabilities() & DNSBackend::CAP_VIEWS) == 0) { + cerr << "None of the configured backends support views." << endl; + return 1; + } + string view{cmds.at(0)}; + string error; + if (!Check::validateViewName(view, error)) { + cerr << error << "." << endl; + return 1; + } ZoneName zone{cmds.at(1)}; if (!B.viewDelZone(view, zone)) { cerr<<"Operation failed."<& cmds, const std::string_view synopsis) UtilBackend B("default"); //NOLINT(readability-identifier-length) + if ((B.getCapabilities() & DNSBackend::CAP_VIEWS) == 0) { + cerr << "None of the configured backends support views." << endl; + return 1; + } + vector > ret; B.networkList(ret); @@ -5533,6 +5569,11 @@ static int setNetwork(vector& cmds, const std::string_view synopsis) UtilBackend B("default"); //NOLINT(readability-identifier-length) + if ((B.getCapabilities() & DNSBackend::CAP_VIEWS) == 0) { + cerr << "None of the configured backends support views." << endl; + return 1; + } + Netmask net{cmds.at(0)}; string view{}; if (cmds.size() > 1) { diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 0d0265bca..07736a134 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -2826,6 +2826,10 @@ static void apiServerViewsPOST(HttpRequest* req, HttpResponse* resp) throw ApiException("Zone " + zonename.toString() + " does not exist"); } std::string view{req->parameters["view"]}; + std::string error; + if (!Check::validateViewName(view, error)) { + throw ApiException(error); + } if (!domainInfo.backend->viewAddZone(view, zonename)) { throw ApiException("Failed to add " + zonename.toString() + " to view " + view); @@ -2850,6 +2854,10 @@ static void apiServerViewsDELETE(HttpRequest* req, HttpResponse* resp) { ZoneData zoneData{req}; std::string view{req->parameters["view"]}; + std::string error; + if (!Check::validateViewName(view, error)) { + throw ApiException(error); + } if (!zoneData.domainInfo.backend->viewDelZone(view, zoneData.zoneName)) { throw ApiException("Failed to remove " + zoneData.zoneName.toString() + " from view " + view); diff --git a/regression-tests/tests/views-management/expected_result b/regression-tests/tests/views-management/expected_result index 966b8a2fd..dc2642384 100644 --- a/regression-tests/tests/views-management/expected_result +++ b/regression-tests/tests/views-management/expected_result @@ -1 +1 @@ -Operation failed. +None of the configured backends support views.