]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Perform character set validation of view names. 16152/head
authorMiod Vallat <miod.vallat@powerdns.com>
Mon, 22 Sep 2025 08:42:47 +0000 (10:42 +0200)
committerMiod Vallat <miod.vallat@powerdns.com>
Mon, 22 Sep 2025 14:03:46 +0000 (16:03 +0200)
Signed-off-by: Miod Vallat <miod.vallat@powerdns.com>
docs/views.rst
pdns/check-zone.cc
pdns/check-zone.hh
pdns/pdnsutil.cc
pdns/ws-auth.cc
regression-tests/tests/views-management/expected_result

index f28ddba8804b0124440591437d3d412bf56e63ab..3fb83ef5f77f034652b2e22090b968c2278302cb 100644 (file)
@@ -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
 --------------------
 
index d26f5a404d941052c71e6456ed2909923789d497..431da983e86e32cd1749691c85aaa8bc840d37e2 100644 (file)
 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<DNSResourceRecord>& oldrrs, vector<DNSResourceRecord>& allrrs, const ZoneName& zone, vector<pair<DNSResourceRecord, string>>& errors)
 {
   // QTypes that MUST NOT have multiple records of the same type in a given RRset.
index 89b4577c5eead6384452e8886a3f8d49a8f44b3f..6d43f09b7928e215da59557b961b6c2cb61c3c19 100644 (file)
  * 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.
index cd3f47aa868c7a3da08e5cb75d8d292bb5c36cf5..06aa5322e88976b76c954a22da4e7fd62bf4da0b 100644 (file)
@@ -5440,6 +5440,11 @@ static int listView(vector<string>& 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<ZoneName> ret;
   B.viewListZones(cmds.at(0), ret);
 
@@ -5457,6 +5462,12 @@ static int listViews(vector<string>& 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<string> ret;
   B.viewList(ret);
 
@@ -5474,7 +5485,17 @@ static int viewAddZone(vector<string>& 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."<<endl;
@@ -5498,7 +5519,17 @@ static int viewDelZone(vector<string>& 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."<<endl;
@@ -5515,6 +5546,11 @@ static int listNetwork(vector<string>& 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<pair<Netmask, string> > ret;
 
   B.networkList(ret);
@@ -5533,6 +5569,11 @@ static int setNetwork(vector<string>& 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) {
index 0d0265bcaacb723735dd9596354cdf4333bbc384..07736a134fd436a4c06d2122e91dc7150dba04c3 100644 (file)
@@ -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);
index 966b8a2fddbc6343da15b97cecceb988677cab91..dc26423849778ed24e1df453f562ea6a9c3f9e99 100644 (file)
@@ -1 +1 @@
-Operation failed.
+None of the configured backends support views.