This is an automated email from the ASF dual-hosted git repository.

d4rkstar pushed a commit to branch 0.9.0
in repository https://gitbox.apache.org/repos/asf/openserverless-operator.git


The following commit(s) were added to refs/heads/0.9.0 by this push:
     new 6b99ae8  fix: validate database and bucket names in user provisioning 
(#121)
6b99ae8 is described below

commit 6b99ae83b37d5fc599fb367c37b47d87ce602aa2
Author: Michele Sciabarra <[email protected]>
AuthorDate: Sat Aug 29 17:23:18 2026 +0200

    fix: validate database and bucket names in user provisioning (#121)
    
    Follow-up to #109, which validated spec.namespace but left two other
    tenant controlled WhiskUser fields flowing unsanitized into shell
    commands executed inside the data pods via kubectl exec.
    
    * spec.postgres.database reached `bash -c "... psql --dbname {db_name}"`
      in exec_psql_command_in_db. Unlike the namespace field it never
      becomes a filename, so it was not even constrained to path safe
      characters.
    * spec.object-storage.{data,route}.bucket reached
      `sh -c "echo '{command}' | weed shell"` in the seaweedfs client,
      where a quote breaks out of the single quoted string.
    
    Adds util.validate_database_name and util.validate_bucket_name next to
    the existing validate_namespace and applies them at the call sites and
    at the shell sinks, plus matching CEL rules on the CRD so the API server
    rejects the payload before the operator sees it. The regexes and the CEL
    rules were cross checked to accept exactly the same inputs.
    
    Also:
    * guards the latent render_kvrocks_script / exec_kvrocks_command pair,
      which repeats the same pattern but is not currently wired into the
      user path
    * makes validate_namespace return False for non string input instead of
      raising TypeError
    * corrects a copy pasted error message referring to exec_mongosh_command
      inside exec_psql_command
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 deploy/nuvolaris-permissions/whisk-user-crd.yaml | 15 +++++++
 nuvolaris/kvrocks.py                             |  6 ++-
 nuvolaris/mongodb.py                             |  8 ++++
 nuvolaris/postgres_operator.py                   | 14 +++++-
 nuvolaris/seaweedfs_util.py                      |  9 ++++
 nuvolaris/util.py                                | 54 ++++++++++++++++++++++++
 6 files changed, 104 insertions(+), 2 deletions(-)

diff --git a/deploy/nuvolaris-permissions/whisk-user-crd.yaml 
b/deploy/nuvolaris-permissions/whisk-user-crd.yaml
index 878805f..9a70eca 100644
--- a/deploy/nuvolaris-permissions/whisk-user-crd.yaml
+++ b/deploy/nuvolaris-permissions/whisk-user-crd.yaml
@@ -125,6 +125,9 @@ spec:
                         bucket:
                           description: MINIO data bucket name
                           type: string
+                          x-kubernetes-validations:
+                            - rule: 
"self.matches('^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$')"
+                              message: "Invalid bucket name"
                       required:
                       - enabled 
                       - bucket 
@@ -139,6 +142,9 @@ spec:
                         bucket:
                           description: name of the bucket
                           type: string                                         
           
+                          x-kubernetes-validations:
+                            - rule: 
"self.matches('^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$')"
+                              message: "Invalid bucket name"
                       required:
                       - enabled 
                       - bucket
@@ -155,6 +161,9 @@ spec:
                     database:
                       description: user dedicated mongodb dataabase name
                       type: string
+                      x-kubernetes-validations:
+                        - rule: 
"self.matches('^[a-zA-Z0-9][_a-zA-Z0-9-]{0,62}$')"
+                          message: "Invalid database name"
                     password:
                       description: user mongodb password
                       type: string
@@ -176,6 +185,9 @@ spec:
                     database:
                       description: user dedicated postgres database
                       type: string
+                      x-kubernetes-validations:
+                        - rule: 
"self.matches('^[a-zA-Z0-9][_a-zA-Z0-9-]{0,62}$')"
+                          message: "Invalid database name"
                     password:
                       description: user mongodb password
                       type: string
@@ -197,6 +209,9 @@ spec:
                     database:
                       description: user dedicated MILVUS database
                       type: string
+                      x-kubernetes-validations:
+                        - rule: 
"self.matches('^[a-zA-Z0-9][_a-zA-Z0-9-]{0,62}$')"
+                          message: "Invalid database name"
                     password:
                       description: user MILVUS password
                       type: string
diff --git a/nuvolaris/kvrocks.py b/nuvolaris/kvrocks.py
index 2987af3..d830284 100644
--- a/nuvolaris/kvrocks.py
+++ b/nuvolaris/kvrocks.py
@@ -146,12 +146,16 @@ def delete(owner=None):
 def render_kvrocks_script(namespace,template,data):
     """
     uses the given template to render a redis-cli script to be executed.
-    """  
+    """
+    if not util.validate_namespace(namespace):
+        raise ValueError(f"Invalid namespace {namespace}")
     out = f"/tmp/__{namespace}_{template}"
     file = ntp.spool_template(template, out, data)
     return os.path.abspath(file)
 
 def exec_kvrocks_command(pod_name,path_to_script):
+    if not os.path.exists(path_to_script):
+        raise ValueError(f"invalid path script in exec_kvrocks_command")
     logging.info(f"passing script {path_to_script} to pod {pod_name}")
     res = kube.kubectl("cp",path_to_script,f"{pod_name}:{path_to_script}")
     res = kube.kubectl("exec","-it",pod_name,"--","/bin/sh","-c",f"cat 
{path_to_script} | /bin/redis-cli")
diff --git a/nuvolaris/mongodb.py b/nuvolaris/mongodb.py
index 58c5ea9..210a008 100644
--- a/nuvolaris/mongodb.py
+++ b/nuvolaris/mongodb.py
@@ -134,6 +134,10 @@ def create_db_user(ucfg: UserConfig, user_metadata: 
UserMetadata):
     database = ucfg.get('mongodb.database')
     logging.info(f"authorizing new mongodb database {database}")
 
+    if not util.validate_database_name(database):
+        logging.error(f"failed to add Mongodb database: invalid database name 
{database}")
+        return None
+
     try:
         data = util.get_mongodb_config_data()
         data["database"]=database
@@ -157,6 +161,10 @@ def create_db_user(ucfg: UserConfig, user_metadata: 
UserMetadata):
 def delete_db_user(namespace, database):
     logging.info(f"removing mongodb database {database}")
 
+    if not util.validate_database_name(database):
+        logging.error(f"failed to remove Mongodb database: invalid database 
name {database}")
+        return None
+
     try:
         data = util.get_mongodb_config_data()
         data["subject"]=namespace
diff --git a/nuvolaris/postgres_operator.py b/nuvolaris/postgres_operator.py
index 7274e87..b11d245 100644
--- a/nuvolaris/postgres_operator.py
+++ b/nuvolaris/postgres_operator.py
@@ -182,7 +182,7 @@ def render_postgres_script(namespace,template,data):
 
 def 
exec_psql_command(pod_name,path_to_psql_script,path_to_pgpass,additional_psql_args=''):
     if not os.path.exists(path_to_psql_script):
-        raise ValueError(f"invalid path script in exec_mongosh_command")
+        raise ValueError(f"invalid path script in exec_psql_command")
     logging.info(f"passing script {path_to_psql_script} to pod {pod_name}")
     res = 
kube.kubectl("cp",path_to_psql_script,f"{pod_name}:{path_to_psql_script}")
     res = kube.kubectl("cp",path_to_pgpass,f"{pod_name}:/tmp/.pgpass")
@@ -200,6 +200,10 @@ def create_db_user(ucfg: UserConfig, user_metadata: 
UserMetadata):
     database = ucfg.get('postgres.database')
     logging.info(f"authorizing new postgres database {database}")
 
+    if not util.validate_database_name(database):
+        logging.error(f"failed to add Postgres database: invalid database name 
{database}")
+        return None
+
     try:
         data = util.get_postgres_config_data()        
         data["database"]=database
@@ -238,6 +242,10 @@ def create_db_user(ucfg: UserConfig, user_metadata: 
UserMetadata):
 def delete_db_user(namespace, database):
     logging.info(f"removing postgres database {database}")
 
+    if not util.validate_database_name(database):
+        logging.error(f"failed to remove Postgres database: invalid database 
name {database}")
+        return None
+
     try:
         data = util.get_postgres_config_data()
         data["username"]=namespace
@@ -318,6 +326,10 @@ def patch(status, action, owner=None):
         operator_util.patch_operator_status(status,'postgres','error')
 
 def 
exec_psql_command_in_db(db_name,pod_name,path_to_psql_script,path_to_pgpass):
+    if not util.validate_database_name(db_name):
+        raise ValueError(f"Invalid database name {db_name}")
+    if not os.path.exists(path_to_psql_script):
+        raise ValueError(f"invalid path script in exec_psql_command_in_db")
     logging.info(f"passing script {path_to_psql_script} to pod {pod_name}")
     res = 
kube.kubectl("cp",path_to_psql_script,f"{pod_name}:{path_to_psql_script}")
     res = kube.kubectl("cp",path_to_pgpass,f"{pod_name}:/tmp/.pgpass")
diff --git a/nuvolaris/seaweedfs_util.py b/nuvolaris/seaweedfs_util.py
index 89be2c1..3c1aebc 100644
--- a/nuvolaris/seaweedfs_util.py
+++ b/nuvolaris/seaweedfs_util.py
@@ -19,6 +19,7 @@
 # to perform various operations
 
 import logging
+import re
 import nuvolaris.config as cfg
 import nuvolaris.template as ntp
 import nuvolaris.util as util
@@ -85,6 +86,10 @@ class SeaweedfsClient:
                 raise SeaweedfsUnauthorizedException()
 
     def _exec_weed_command(self,command):
+        # the command is wrapped in a single quoted shell string, so any quote 
or
+        # shell metacharacter reaching this point would break out of it.
+        if not isinstance(command, str) or re.search(r"[\'\"`$;&|<>\n\r\\]", 
command):
+            raise ValueError("invalid characters in weed shell command")
         logging.debug(f"executing command: {command} inside pod 
{self.pod_name}")
         res = 
kube.kubectl("exec","-it",self.pod_name,"--","/bin/sh","-c",f"echo '{command}' 
| weed shell")
         return res                               
@@ -93,6 +98,8 @@ class SeaweedfsClient:
         """
         adds a new bucket inside the configured seaweed instance 
         """
+        if not util.validate_bucket_name(bucket_name):
+            raise ValueError(f"Invalid bucket name {bucket_name}")
         res = util.check(self._exec_weed_command(f"s3.bucket.create -name 
{bucket_name}"),"make_bucket",True)
         if quota_in_mb:
             res = util.check(self._exec_weed_command(f"s3.bucket.quota -name 
{bucket_name} -op=set -sizeMB={quota_in_mb}"),"make_bucket",res)
@@ -102,6 +109,8 @@ class SeaweedfsClient:
         """
         removes unconditionally a bucket
         """
+        if not util.validate_bucket_name(bucket_name):
+            raise ValueError(f"Invalid bucket name {bucket_name}")
         return util.check(self._exec_weed_command(f"s3.bucket.delete -name 
{bucket_name}"),"force_bucket_remove",True)
 
     def upload_folder_content(self,local_content,bucket):
diff --git a/nuvolaris/util.py b/nuvolaris/util.py
index 34a1bb2..aa7667d 100644
--- a/nuvolaris/util.py
+++ b/nuvolaris/util.py
@@ -394,10 +394,64 @@ def validate_namespace(namespace: str) -> bool:
         True
         >>> util.validate_namespace('x;id;#')
         False
+        >>> util.validate_namespace(None)
+        False
     """
+    if not isinstance(namespace, str):
+        return False
+
     NAMESPACE_RE = re.compile(r"^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$")
     return bool(NAMESPACE_RE.fullmatch(namespace))
 
+def validate_database_name(database: str) -> bool:
+    """
+        Validates a user provided database name. Database names are 
interpolated
+        into shell commands executed inside the database pods, so they must be
+        restricted to a safe character set.
+        >>> import nuvolaris.util as util
+        >>> util.validate_database_name("demodb")
+        True
+        >>> util.validate_database_name("demo_db1")
+        True
+        >>> util.validate_database_name('x; id; #')
+        False
+        >>> util.validate_database_name('')
+        False
+        >>> util.validate_database_name(None)
+        False
+    """
+    if not isinstance(database, str):
+        return False
+
+    DATABASE_RE = re.compile(r"^[a-zA-Z0-9](?:[_a-zA-Z0-9-]{0,62})?$")
+    return bool(DATABASE_RE.fullmatch(database))
+
+def validate_bucket_name(bucket: str) -> bool:
+    """
+        Validates a user provided bucket name against the S3 bucket naming 
rules.
+        Bucket names are interpolated into shell commands executed inside the
+        storage pods, so they must be restricted to a safe character set.
+        >>> import nuvolaris.util as util
+        >>> util.validate_bucket_name("demo-bucket")
+        True
+        >>> util.validate_bucket_name("demo.bucket1")
+        True
+        >>> util.validate_bucket_name("x'; id; #")
+        False
+        >>> util.validate_bucket_name('')
+        False
+        >>> util.validate_bucket_name(None)
+        False
+    """
+    if not isinstance(bucket, str):
+        return False
+
+    if len(bucket) < 3 or len(bucket) > 63:
+        return False
+
+    BUCKET_RE = re.compile(r"^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$")
+    return bool(BUCKET_RE.fullmatch(bucket))
+
 def validate_ow_auth(auth):
     """
         >>> import nuvolaris.testutil as tutil

Reply via email to