Skip to content

Conversation

@ad-astra-video
Copy link
Collaborator

@ad-astra-video ad-astra-video commented Mar 18, 2025

Adds API routes to manage custom nodes and models in comfystream.

Includes a hot reload route /settings/reload to close all connections and reload the Pipeline. The hot reload required copying the import_all_nodes_in_workspace function from hiddenswitch/comfyui to remove the check if nodes are already imported.

The managment API routes are included in the comfystream aiohttp server. Calling the routes can cause delays in frame processing because I think some of the underlying calls are not async.

Routes are as follows:

  • /settings/nodes/{list/install/delete}
  • /settings/models/{list/add/delete}
  • /settings/reload
  • /settings/turn/server/set/account

@ad-astra-video
Copy link
Collaborator Author

@hjpotter92 moved the PR here. I included update you suggested to have the routes under /settings/...

@ad-astra-video ad-astra-video changed the title feature: add management api for comfystreamComfystream mgmt api feature: add management api for Comfystream mgmt api Mar 31, 2025
@ad-astra-video ad-astra-video force-pushed the comfystream-mgmt-api branch 2 times, most recently from e711d6d to 9c23759 Compare April 14, 2025 16:59
@ad-astra-video ad-astra-video force-pushed the comfystream-mgmt-api branch from aab2a80 to d3993eb Compare May 14, 2025 22:49
logger.info(f"model path: {model_path}")

# check path is in workspace_dir, raises value error if not
model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To address the issue, we will:

  1. Normalize the model_path using Path.resolve() to eliminate any .. segments or symbolic links.
  2. Explicitly check that the normalized path starts with the expected root directory (workspace_dir/models) to ensure it does not escape the intended directory structure.
  3. Apply these changes in both the add_model and delete_model functions, as they both construct paths using user-provided data.

Suggested changeset 1
server/api/models/models.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/models/models.py b/server/api/models/models.py
--- a/server/api/models/models.py
+++ b/server/api/models/models.py
@@ -79,4 +79,7 @@
         
-        # check path is in workspace_dir, raises value error if not
-        model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
+        # Normalize and validate the path
+        model_path = model_path.resolve()
+        root_path = Path(os.path.join(workspace_dir, "models")).resolve()
+        if not str(model_path).startswith(str(root_path)):
+            raise ValueError(f"Invalid model path: {model_path} is outside the allowed directory")
         os.makedirs(model_path.parent, exist_ok=True)
@@ -95,4 +98,7 @@
         model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
-        #check path is in workspace_dir, raises value error if not
-        model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
+        # Normalize and validate the path
+        model_path = model_path.resolve()
+        root_path = Path(os.path.join(workspace_dir, "models")).resolve()
+        if not str(model_path).startswith(str(root_path)):
+            raise ValueError(f"Invalid model path: {model_path} is outside the allowed directory")
         
EOF
@@ -79,4 +79,7 @@

# check path is in workspace_dir, raises value error if not
model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
# Normalize and validate the path
model_path = model_path.resolve()
root_path = Path(os.path.join(workspace_dir, "models")).resolve()
if not str(model_path).startswith(str(root_path)):
raise ValueError(f"Invalid model path: {model_path} is outside the allowed directory")
os.makedirs(model_path.parent, exist_ok=True)
@@ -95,4 +98,7 @@
model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
#check path is in workspace_dir, raises value error if not
model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
# Normalize and validate the path
model_path = model_path.resolve()
root_path = Path(os.path.join(workspace_dir, "models")).resolve()
if not str(model_path).startswith(str(root_path)):
raise ValueError(f"Invalid model path: {model_path} is outside the allowed directory")

Copilot is powered by AI and may make mistakes. Always verify output.

# check path is in workspace_dir, raises value error if not
model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
os.makedirs(model_path.parent, exist_ok=True)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the issue, we will sanitize and validate the user-provided path (model['path']) before constructing the model_path. Specifically:

  1. Use os.path.normpath to normalize the path and remove any .. segments.
  2. Ensure that the normalized path does not escape the intended directory structure by checking that it starts with the workspace_dir after normalization.
  3. Apply these changes in the add_model and delete_model functions in server/api/models/models.py.

Suggested changeset 1
server/api/models/models.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/models/models.py b/server/api/models/models.py
--- a/server/api/models/models.py
+++ b/server/api/models/models.py
@@ -74,5 +74,8 @@
         model_path = Path(os.path.join(workspace_dir, "models", model['type'], model_name))
-        #if specified, use the model path from the model dict (e.g. sd1.5/model.safetensors will put model.safetensors in models/checkpoints/sd1.5)
+        # if specified, use the model path from the model dict (e.g. sd1.5/model.safetensors will put model.safetensors in models/checkpoints/sd1.5)
         if 'path' in model:
-            model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
+            user_path = os.path.normpath(model['path'])  # Normalize the user-provided path
+            if user_path.startswith("..") or os.path.isabs(user_path):  # Prevent directory traversal or absolute paths
+                raise ValueError("Invalid model path: directory traversal or absolute paths are not allowed")
+            model_path = Path(os.path.join(workspace_dir, "models", model['type'], user_path))
             logger.info(f"model path: {model_path}")
@@ -94,4 +97,7 @@
     try:
-        model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
-        #check path is in workspace_dir, raises value error if not
+        user_path = os.path.normpath(model['path'])  # Normalize the user-provided path
+        if user_path.startswith("..") or os.path.isabs(user_path):  # Prevent directory traversal or absolute paths
+            raise ValueError("Invalid model path: directory traversal or absolute paths are not allowed")
+        model_path = Path(os.path.join(workspace_dir, "models", model['type'], user_path))
+        # check path is in workspace_dir, raises value error if not
         model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
EOF
@@ -74,5 +74,8 @@
model_path = Path(os.path.join(workspace_dir, "models", model['type'], model_name))
#if specified, use the model path from the model dict (e.g. sd1.5/model.safetensors will put model.safetensors in models/checkpoints/sd1.5)
# if specified, use the model path from the model dict (e.g. sd1.5/model.safetensors will put model.safetensors in models/checkpoints/sd1.5)
if 'path' in model:
model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
user_path = os.path.normpath(model['path']) # Normalize the user-provided path
if user_path.startswith("..") or os.path.isabs(user_path): # Prevent directory traversal or absolute paths
raise ValueError("Invalid model path: directory traversal or absolute paths are not allowed")
model_path = Path(os.path.join(workspace_dir, "models", model['type'], user_path))
logger.info(f"model path: {model_path}")
@@ -94,4 +97,7 @@
try:
model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
#check path is in workspace_dir, raises value error if not
user_path = os.path.normpath(model['path']) # Normalize the user-provided path
if user_path.startswith("..") or os.path.isabs(user_path): # Prevent directory traversal or absolute paths
raise ValueError("Invalid model path: directory traversal or absolute paths are not allowed")
model_path = Path(os.path.join(workspace_dir, "models", model['type'], user_path))
# check path is in workspace_dir, raises value error if not
model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
Copilot is powered by AI and may make mistakes. Always verify output.
# start downloading the model in background without blocking
asyncio.create_task(download_model(model['url'], model_path))
except Exception as e:
os.remove(model_path)+".downloading"

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the issue, we need to ensure that the model_path is validated and sanitized before being used in the os.remove operation. Specifically:

  1. Use os.path.normpath or Path.resolve() to normalize the path and remove any .. segments.
  2. Ensure that the normalized path is within the workspace_dir using the relative_to check.
  3. Add a fallback mechanism to handle cases where the path validation fails, ensuring no unintended file operations occur.

The fix will involve:

  • Adding a validation step before the os.remove operation on line 86.
  • Ensuring that the .downloading suffix is appended to the validated path in a safe manner.

Suggested changeset 1
server/api/models/models.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/models/models.py b/server/api/models/models.py
--- a/server/api/models/models.py
+++ b/server/api/models/models.py
@@ -85,3 +85,8 @@
     except Exception as e:
-        os.remove(model_path)+".downloading"
+        try:
+            # Validate the model_path before removing
+            validated_path = model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
+            os.remove(validated_path.with_suffix(".downloading"))
+        except Exception as validation_error:
+            logger.error(f"Failed to validate or remove path: {validation_error}")
         raise Exception(f"error downloading model: {e}")
EOF
@@ -85,3 +85,8 @@
except Exception as e:
os.remove(model_path)+".downloading"
try:
# Validate the model_path before removing
validated_path = model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
os.remove(validated_path.with_suffix(".downloading"))
except Exception as validation_error:
logger.error(f"Failed to validate or remove path: {validation_error}")
raise Exception(f"error downloading model: {e}")
Copilot is powered by AI and may make mistakes. Always verify output.
try:
model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
#check path is in workspace_dir, raises value error if not
model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the issue, we need to ensure that the model_path is normalized before performing the relative_to check. This can be achieved by using Path.resolve() to normalize the path and remove any .. segments or symbolic links. Additionally, we should ensure that the normalized path starts with the intended base directory (workspace_dir/models) to prevent path traversal attacks.

Steps to fix:

  1. Normalize the model_path using Path.resolve() before performing the relative_to check.
  2. Ensure that the normalized path starts with the intended base directory (workspace_dir/models).
  3. Update the delete_model function in server/api/models/models.py to include these changes.

Suggested changeset 1
server/api/models/models.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/models/models.py b/server/api/models/models.py
--- a/server/api/models/models.py
+++ b/server/api/models/models.py
@@ -94,5 +94,7 @@
     try:
-        model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
-        #check path is in workspace_dir, raises value error if not
-        model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
+        model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path'])).resolve()
+        base_path = Path(os.path.join(workspace_dir, "models")).resolve()
+        # Ensure the normalized path is within the intended base directory
+        if not str(model_path).startswith(str(base_path)):
+            raise ValueError("Invalid path: Path traversal attempt detected")
         
EOF
@@ -94,5 +94,7 @@
try:
model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
#check path is in workspace_dir, raises value error if not
model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path'])).resolve()
base_path = Path(os.path.join(workspace_dir, "models")).resolve()
# Ensure the normalized path is within the intended base directory
if not str(model_path).startswith(str(base_path)):
raise ValueError("Invalid path: Path traversal attempt detected")

Copilot is powered by AI and may make mistakes. Always verify output.
#check path is in workspace_dir, raises value error if not
model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))

os.remove(model_path)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the issue, we will enhance the validation of model_path to ensure it is safe and contained within the workspace_dir. Specifically:

  1. Use os.path.realpath to resolve the absolute path of model_path and compare it with the absolute path of the workspace_dir to prevent symbolic link attacks.
  2. Perform the validation immediately before the os.remove operation to minimize the risk of race conditions.
  3. Add a fallback exception to handle any unexpected edge cases.

Suggested changeset 1
server/api/models/models.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/models/models.py b/server/api/models/models.py
--- a/server/api/models/models.py
+++ b/server/api/models/models.py
@@ -95,6 +95,10 @@
         model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
-        #check path is in workspace_dir, raises value error if not
-        model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
+        # Ensure the resolved path is within the workspace_dir
+        full_model_path = os.path.realpath(model_path)
+        base_models_dir = os.path.realpath(os.path.join(workspace_dir, "models"))
+        if not full_model_path.startswith(base_models_dir):
+            raise Exception("Invalid model path: Path traversal detected")
         
-        os.remove(model_path)
+        # Safely remove the file
+        os.remove(full_model_path)
     except Exception as e:
EOF
@@ -95,6 +95,10 @@
model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path']))
#check path is in workspace_dir, raises value error if not
model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models")))
# Ensure the resolved path is within the workspace_dir
full_model_path = os.path.realpath(model_path)
base_models_dir = os.path.realpath(os.path.join(workspace_dir, "models"))
if not full_model_path.startswith(base_models_dir):
raise Exception("Invalid model path: Path traversal detected")

os.remove(model_path)
# Safely remove the file
os.remove(full_model_path)
except Exception as e:
Copilot is powered by AI and may make mistakes. Always verify output.
try:
#delete the folder and all its contents. ignore_errors allows readonly files to be deleted
logger.info(f"deleting node {node['name']}")
shutil.rmtree(node_path, ignore_errors=True)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the issue, we need to validate the constructed node_path to ensure it is confined within the custom_nodes_path directory. This can be achieved by normalizing the path using os.path.normpath or Path.resolve() and verifying that the resulting path starts with custom_nodes_path. If the validation fails, an exception should be raised to prevent unsafe operations.

Changes will be made in the delete_node and toggle_node functions in server/api/nodes/nodes.py to validate the node_path before performing any operations.


Suggested changeset 1
server/api/nodes/nodes.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py
--- a/server/api/nodes/nodes.py
+++ b/server/api/nodes/nodes.py
@@ -122,3 +122,5 @@
     
-    node_path = custom_nodes_path / node["name"]
+    node_path = (custom_nodes_path / node["name"]).resolve()
+    if not str(node_path).startswith(str(custom_nodes_path)):
+        raise ValueError(f"Invalid node name: {node['name']}")
     if not node_path.exists():
@@ -141,3 +143,5 @@
     
-    node_path = custom_nodes_path / node["name"]
+    node_path = (custom_nodes_path / node["name"]).resolve()
+    if not str(node_path).startswith(str(custom_nodes_path)):
+        raise ValueError(f"Invalid node name: {node['name']}")
     is_disabled = False
@@ -147,3 +151,5 @@
         #try with .disabled
-        node_path = custom_nodes_path / str(node['name']+".disabled")
+        node_path = (custom_nodes_path / str(node['name']+".disabled")).resolve()
+        if not str(node_path).startswith(str(custom_nodes_path)):
+            raise ValueError(f"Invalid node name: {node['name']}.disabled")
         logger.info(f"checking if node { node['name'] } is disabled")
EOF
@@ -122,3 +122,5 @@

node_path = custom_nodes_path / node["name"]
node_path = (custom_nodes_path / node["name"]).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
if not node_path.exists():
@@ -141,3 +143,5 @@

node_path = custom_nodes_path / node["name"]
node_path = (custom_nodes_path / node["name"]).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
is_disabled = False
@@ -147,3 +151,5 @@
#try with .disabled
node_path = custom_nodes_path / str(node['name']+".disabled")
node_path = (custom_nodes_path / str(node['name']+".disabled")).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}.disabled")
logger.info(f"checking if node { node['name'] } is disabled")
Copilot is powered by AI and may make mistakes. Always verify output.
is_disabled = False
#confirm if enabled node exists
logger.info(f"toggling node { node['name'] }")
if not node_path.exists():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the issue, we need to validate and sanitize the node["name"] value before using it to construct file paths. The best approach is to:

  1. Normalize the constructed path using os.path.normpath or Path.resolve() to eliminate any .. segments.
  2. Ensure that the resulting path is contained within the custom_nodes_path directory by checking that it starts with or is a subpath of custom_nodes_path.
  3. Optionally, use a stricter validation mechanism (e.g., werkzeug.utils.secure_filename) to ensure that node["name"] is a valid and safe directory name.

The changes will be applied to the delete_node and toggle_node functions in server/api/nodes/nodes.py.


Suggested changeset 1
server/api/nodes/nodes.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py
--- a/server/api/nodes/nodes.py
+++ b/server/api/nodes/nodes.py
@@ -122,3 +122,5 @@
     
-    node_path = custom_nodes_path / node["name"]
+    node_path = (custom_nodes_path / node["name"]).resolve()
+    if not str(node_path).startswith(str(custom_nodes_path)):
+        raise ValueError(f"Invalid node name: {node['name']}")
     if not node_path.exists():
@@ -141,3 +143,5 @@
     
-    node_path = custom_nodes_path / node["name"]
+    node_path = (custom_nodes_path / node["name"]).resolve()
+    if not str(node_path).startswith(str(custom_nodes_path)):
+        raise ValueError(f"Invalid node name: {node['name']}")
     is_disabled = False
EOF
@@ -122,3 +122,5 @@

node_path = custom_nodes_path / node["name"]
node_path = (custom_nodes_path / node["name"]).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
if not node_path.exists():
@@ -141,3 +143,5 @@

node_path = custom_nodes_path / node["name"]
node_path = (custom_nodes_path / node["name"]).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
is_disabled = False
Copilot is powered by AI and may make mistakes. Always verify output.
#try with .disabled
node_path = custom_nodes_path / str(node['name']+".disabled")
logger.info(f"checking if node { node['name'] } is disabled")
if not node_path.exists():

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the issue, we need to validate and sanitize the node["name"] value before using it in path operations. The best approach is to normalize the constructed path and ensure it remains within the custom_nodes_path directory. This can be achieved using os.path.normpath or Path.resolve() to handle any .. segments in the path and then verifying that the resulting path starts with custom_nodes_path.

Steps to implement the fix:

  1. Normalize the constructed path using Path.resolve() to eliminate any .. segments.
  2. Check that the normalized path is a subpath of custom_nodes_path.
  3. Raise an exception if the validation fails.

Suggested changeset 1
server/api/nodes/nodes.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py
--- a/server/api/nodes/nodes.py
+++ b/server/api/nodes/nodes.py
@@ -122,3 +122,5 @@
     
-    node_path = custom_nodes_path / node["name"]
+    node_path = (custom_nodes_path / node["name"]).resolve()
+    if not str(node_path).startswith(str(custom_nodes_path)):
+        raise ValueError(f"Invalid node name: {node['name']}")
     if not node_path.exists():
@@ -141,3 +143,5 @@
     
-    node_path = custom_nodes_path / node["name"]
+    node_path = (custom_nodes_path / node["name"]).resolve()
+    if not str(node_path).startswith(str(custom_nodes_path)):
+        raise ValueError(f"Invalid node name: {node['name']}")
     is_disabled = False
@@ -147,3 +151,5 @@
         #try with .disabled
-        node_path = custom_nodes_path / str(node['name']+".disabled")
+        node_path = (custom_nodes_path / str(node['name']+".disabled")).resolve()
+        if not str(node_path).startswith(str(custom_nodes_path)):
+            raise ValueError(f"Invalid node name: {node['name']}")
         logger.info(f"checking if node { node['name'] } is disabled")
EOF
@@ -122,3 +122,5 @@

node_path = custom_nodes_path / node["name"]
node_path = (custom_nodes_path / node["name"]).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
if not node_path.exists():
@@ -141,3 +143,5 @@

node_path = custom_nodes_path / node["name"]
node_path = (custom_nodes_path / node["name"]).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
is_disabled = False
@@ -147,3 +151,5 @@
#try with .disabled
node_path = custom_nodes_path / str(node['name']+".disabled")
node_path = (custom_nodes_path / str(node['name']+".disabled")).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
logger.info(f"checking if node { node['name'] } is disabled")
Copilot is powered by AI and may make mistakes. Always verify output.
#rename the folder to remove .disabled
logger.info(f"enabling node {node['name']}")
new_name = node_path.with_name(node["name"])
shutil.move(str(node_path), str(new_name))

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the issue, we need to validate and sanitize the node["name"] value before using it in path expressions. The best approach is to:

  1. Normalize the constructed path using os.path.normpath or Path.resolve() to eliminate any .. segments.
  2. Ensure that the resulting path is within the intended custom_nodes_path directory by checking that it starts with the custom_nodes_path prefix.
  3. Optionally, use a stricter validation mechanism, such as werkzeug.utils.secure_filename, to ensure the node["name"] value does not contain special characters or unexpected patterns.

The changes will be applied to the delete_node and toggle_node functions in server/api/nodes/nodes.py.


Suggested changeset 1
server/api/nodes/nodes.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py
--- a/server/api/nodes/nodes.py
+++ b/server/api/nodes/nodes.py
@@ -122,3 +122,5 @@
     
-    node_path = custom_nodes_path / node["name"]
+    node_path = (custom_nodes_path / node["name"]).resolve()
+    if not str(node_path).startswith(str(custom_nodes_path)):
+        raise ValueError(f"Invalid node name: {node['name']}")
     if not node_path.exists():
@@ -141,3 +143,5 @@
     
-    node_path = custom_nodes_path / node["name"]
+    node_path = (custom_nodes_path / node["name"]).resolve()
+    if not str(node_path).startswith(str(custom_nodes_path)):
+        raise ValueError(f"Invalid node name: {node['name']}")
     is_disabled = False
@@ -147,3 +151,5 @@
         #try with .disabled
-        node_path = custom_nodes_path / str(node['name']+".disabled")
+        node_path = (custom_nodes_path / str(node['name']+".disabled")).resolve()
+        if not str(node_path).startswith(str(custom_nodes_path)):
+            raise ValueError(f"Invalid node name: {node['name']}")
         logger.info(f"checking if node { node['name'] } is disabled")
EOF
@@ -122,3 +122,5 @@

node_path = custom_nodes_path / node["name"]
node_path = (custom_nodes_path / node["name"]).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
if not node_path.exists():
@@ -141,3 +143,5 @@

node_path = custom_nodes_path / node["name"]
node_path = (custom_nodes_path / node["name"]).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
is_disabled = False
@@ -147,3 +151,5 @@
#try with .disabled
node_path = custom_nodes_path / str(node['name']+".disabled")
node_path = (custom_nodes_path / str(node['name']+".disabled")).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
raise ValueError(f"Invalid node name: {node['name']}")
logger.info(f"checking if node { node['name'] } is disabled")
Copilot is powered by AI and may make mistakes. Always verify output.
#rename the folder to add .disabled
logger.info(f"disbling node {node['name']}")
new_name = node_path.with_name(node["name"]+".disabled")
shutil.move(str(node_path), str(new_name))

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the issue, we need to validate and sanitize the user-provided node["name"] to ensure it does not contain malicious characters or sequences that could lead to path traversal attacks. The best approach is to:

  1. Normalize the constructed path using os.path.normpath or Path.resolve() to eliminate any .. segments.
  2. Verify that the normalized path is within the intended custom_nodes_path directory.
  3. Optionally, use a stricter validation mechanism (e.g., werkzeug.utils.secure_filename) to ensure the node["name"] is a valid filename.

The changes will be made in the toggle_node function in server/api/nodes/nodes.py.


Suggested changeset 1
server/api/nodes/nodes.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py
--- a/server/api/nodes/nodes.py
+++ b/server/api/nodes/nodes.py
@@ -141,20 +141,30 @@
     
-    node_path = custom_nodes_path / node["name"]
+    # Sanitize and validate the node name
+    node_name = node["name"]
+    if not isinstance(node_name, str) or ".." in node_name or "/" in node_name or "\\" in node_name:
+        logger.error(f"Invalid node name: {node_name}")
+        raise ValueError("Invalid node name")
+    
+    node_path = (custom_nodes_path / node_name).resolve()
+    if not str(node_path).startswith(str(custom_nodes_path)):
+        logger.error(f"Path traversal attempt detected: {node_path}")
+        raise ValueError("Invalid node path")
+    
     is_disabled = False
     #confirm if enabled node exists
-    logger.info(f"toggling node { node['name'] }")
+    logger.info(f"toggling node {node_name}")
     if not node_path.exists():
         #try with .disabled
-        node_path = custom_nodes_path / str(node['name']+".disabled")
-        logger.info(f"checking if node { node['name'] } is disabled")
+        node_path = (custom_nodes_path / f"{node_name}.disabled").resolve()
+        logger.info(f"checking if node {node_name} is disabled")
         if not node_path.exists():
             #node does not exist as enabled or disabled
-            logger.info(f"node { node['name'] }.disabled does not exist")
-            raise ValueError(f"node { node['name'] } does not exist")
+            logger.info(f"node {node_name}.disabled does not exist")
+            raise ValueError(f"node {node_name} does not exist")
         else:
             #node is disabled, so we need to enable it
-            logger.error(f"node { node['name'] } is disabled")
+            logger.error(f"node {node_name} is disabled")
             is_disabled = True
     else:
-        logger.info(f"node { node['name'] } is enabled")
+        logger.info(f"node {node_name} is enabled")
 
@@ -163,4 +173,4 @@
             #rename the folder to remove .disabled
-            logger.info(f"enabling node {node['name']}")
-            new_name = node_path.with_name(node["name"])
+            logger.info(f"enabling node {node_name}")
+            new_name = node_path.with_name(node_name)
             shutil.move(str(node_path), str(new_name))
@@ -168,8 +178,8 @@
             #rename the folder to add .disabled
-            logger.info(f"disbling node {node['name']}")
-            new_name = node_path.with_name(node["name"]+".disabled")
+            logger.info(f"disabling node {node_name}")
+            new_name = node_path.with_name(f"{node_name}.disabled")
             shutil.move(str(node_path), str(new_name))
     except Exception as e:
-        logger.error(f"error {action} node {node['name']}: {e}")
-        raise Exception(f"error {action} node: {e}")
+        logger.error(f"error toggling node {node_name}: {e}")
+        raise Exception(f"error toggling node: {e}")
 
EOF
@@ -141,20 +141,30 @@

node_path = custom_nodes_path / node["name"]
# Sanitize and validate the node name
node_name = node["name"]
if not isinstance(node_name, str) or ".." in node_name or "/" in node_name or "\\" in node_name:
logger.error(f"Invalid node name: {node_name}")
raise ValueError("Invalid node name")

node_path = (custom_nodes_path / node_name).resolve()
if not str(node_path).startswith(str(custom_nodes_path)):
logger.error(f"Path traversal attempt detected: {node_path}")
raise ValueError("Invalid node path")

is_disabled = False
#confirm if enabled node exists
logger.info(f"toggling node { node['name'] }")
logger.info(f"toggling node {node_name}")
if not node_path.exists():
#try with .disabled
node_path = custom_nodes_path / str(node['name']+".disabled")
logger.info(f"checking if node { node['name'] } is disabled")
node_path = (custom_nodes_path / f"{node_name}.disabled").resolve()
logger.info(f"checking if node {node_name} is disabled")
if not node_path.exists():
#node does not exist as enabled or disabled
logger.info(f"node { node['name'] }.disabled does not exist")
raise ValueError(f"node { node['name'] } does not exist")
logger.info(f"node {node_name}.disabled does not exist")
raise ValueError(f"node {node_name} does not exist")
else:
#node is disabled, so we need to enable it
logger.error(f"node { node['name'] } is disabled")
logger.error(f"node {node_name} is disabled")
is_disabled = True
else:
logger.info(f"node { node['name'] } is enabled")
logger.info(f"node {node_name} is enabled")

@@ -163,4 +173,4 @@
#rename the folder to remove .disabled
logger.info(f"enabling node {node['name']}")
new_name = node_path.with_name(node["name"])
logger.info(f"enabling node {node_name}")
new_name = node_path.with_name(node_name)
shutil.move(str(node_path), str(new_name))
@@ -168,8 +178,8 @@
#rename the folder to add .disabled
logger.info(f"disbling node {node['name']}")
new_name = node_path.with_name(node["name"]+".disabled")
logger.info(f"disabling node {node_name}")
new_name = node_path.with_name(f"{node_name}.disabled")
shutil.move(str(node_path), str(new_name))
except Exception as e:
logger.error(f"error {action} node {node['name']}: {e}")
raise Exception(f"error {action} node: {e}")
logger.error(f"error toggling node {node_name}: {e}")
raise Exception(f"error toggling node: {e}")

Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Collaborator

@eliteprox eliteprox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @ad-astra-video for continuing to work on this, I'd like to merge this within the next week. Looks like there are a few path traversal issues to address. If you can take care of those, will give re-review and merge. We can build additional functionality around node and model installation once this PR is merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants