-
Notifications
You must be signed in to change notification settings - Fork 6
feature: add management api for Comfystream mgmt api #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
@hjpotter92 moved the PR here. I included update you suggested to have the routes under |
e711d6d to
9c23759
Compare
aab2a80 to
d3993eb
Compare
| 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
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 7 months ago
To address the issue, we will:
- Normalize the
model_pathusingPath.resolve()to eliminate any..segments or symbolic links. - 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. - Apply these changes in both the
add_modelanddelete_modelfunctions, as they both construct paths using user-provided data.
-
Copy modified lines R80-R84 -
Copy modified lines R99-R103
| @@ -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") | ||
|
|
|
|
||
| # 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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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:
- Use
os.path.normpathto normalize the path and remove any..segments. - Ensure that the normalized path does not escape the intended directory structure by checking that it starts with the
workspace_dirafter normalization. - Apply these changes in the
add_modelanddelete_modelfunctions inserver/api/models/models.py.
-
Copy modified line R75 -
Copy modified lines R77-R80 -
Copy modified lines R98-R102
| @@ -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"))) |
| # 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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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:
- Use
os.path.normpathorPath.resolve()to normalize the path and remove any..segments. - Ensure that the normalized path is within the
workspace_dirusing therelative_tocheck. - 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.removeoperation on line 86. - Ensuring that the
.downloadingsuffix is appended to the validated path in a safe manner.
-
Copy modified lines R86-R91
| @@ -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}") |
| 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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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:
- Normalize the
model_pathusingPath.resolve()before performing therelative_tocheck. - Ensure that the normalized path starts with the intended base directory (
workspace_dir/models). - Update the
delete_modelfunction inserver/api/models/models.pyto include these changes.
-
Copy modified lines R95-R99
| @@ -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") | ||
|
|
| #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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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:
- Use
os.path.realpathto resolve the absolute path ofmodel_pathand compare it with the absolute path of theworkspace_dirto prevent symbolic link attacks. - Perform the validation immediately before the
os.removeoperation to minimize the risk of race conditions. - Add a fallback exception to handle any unexpected edge cases.
-
Copy modified lines R96-R100 -
Copy modified lines R102-R103
| @@ -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: |
| 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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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.
-
Copy modified lines R123-R125 -
Copy modified lines R144-R146 -
Copy modified lines R152-R154
| @@ -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") |
| 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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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:
- Normalize the constructed path using
os.path.normpathorPath.resolve()to eliminate any..segments. - Ensure that the resulting path is contained within the
custom_nodes_pathdirectory by checking that it starts with or is a subpath ofcustom_nodes_path. - Optionally, use a stricter validation mechanism (e.g.,
werkzeug.utils.secure_filename) to ensure thatnode["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.
-
Copy modified lines R123-R125 -
Copy modified lines R144-R146
| @@ -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 |
| #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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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:
- Normalize the constructed path using
Path.resolve()to eliminate any..segments. - Check that the normalized path is a subpath of
custom_nodes_path. - Raise an exception if the validation fails.
-
Copy modified lines R123-R125 -
Copy modified lines R144-R146 -
Copy modified lines R152-R154
| @@ -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") |
| #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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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:
- Normalize the constructed path using
os.path.normpathorPath.resolve()to eliminate any..segments. - Ensure that the resulting path is within the intended
custom_nodes_pathdirectory by checking that it starts with thecustom_nodes_pathprefix. - Optionally, use a stricter validation mechanism, such as
werkzeug.utils.secure_filename, to ensure thenode["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.
-
Copy modified lines R123-R125 -
Copy modified lines R144-R146 -
Copy modified lines R152-R154
| @@ -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") |
| #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
user-provided value
Show autofix suggestion
Hide autofix suggestion
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:
- Normalize the constructed path using
os.path.normpathorPath.resolve()to eliminate any..segments. - Verify that the normalized path is within the intended
custom_nodes_pathdirectory. - Optionally, use a stricter validation mechanism (e.g.,
werkzeug.utils.secure_filename) to ensure thenode["name"]is a valid filename.
The changes will be made in the toggle_node function in server/api/nodes/nodes.py.
-
Copy modified lines R142-R152 -
Copy modified line R155 -
Copy modified lines R158-R159 -
Copy modified lines R162-R163 -
Copy modified line R166 -
Copy modified line R169 -
Copy modified lines R174-R175 -
Copy modified lines R179-R180 -
Copy modified lines R183-R184
| @@ -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}") | ||
|
|
eliteprox
left a comment
There was a problem hiding this 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
Adds API routes to manage custom nodes and models in comfystream.
Includes a hot reload route
/settings/reloadto close all connections and reload thePipeline. The hot reload required copying theimport_all_nodes_in_workspacefunction 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