# ====================================================================== # Node Pack: artyclaw-comfy # Author: artyclaw # Version: 1.0.1 # Description: A power-user toolkit for prompt logic, IO, and RNG. # Changes: # - 1.0.1: Security fix - Restricted file writing to the ComfyUI output directory # to prevent arbitrary path access. # ====================================================================== import os import re from datetime import datetime import traceback import folder_paths NODE_NAME = "DebugTextSaver" class DebugTextSaver: @classmethod def INPUT_TYPES(s): return { "required": { "string": ("STRING", {"multiline": True, "default": ""}), "output_subfolder": ("STRING", {"default": "debug"}), "file_name": ("STRING", {"default": "debug_log"}), }, "optional": { "file_extension": ("STRING", {"default": "txt"}), "append": ("BOOLEAN", {"default": False}), } } RETURN_TYPES = ("STRING",) RETURN_NAMES = ("file_path",) FUNCTION = "save" CATEGORY = "text/utils" def save(self, string, output_subfolder="debug", file_name="debug_log", file_extension="txt", append=False): print("[DebugTextSaver] --- run start ---") # Ensure we have a str if not isinstance(string, str): string = str(string) def normalize_escapes(s: str) -> str: # Convert common literal escapes to real characters s = s.replace('\\r\\n', '\n').replace('\\n', '\n').replace('\\r', '\n').replace('\\t', '\t') # Collapse double backslashes -> single backslash s = s.replace('\\\\', '\\') return s clean_text = normalize_escapes(string) # Normalize any remaining CR/LF to single \n clean_text = re.sub(r'\r\n?', '\n', clean_text) # Timestamp header timestamp = datetime.now().strftime("[%Y-%m-%d %H:%M:%S]") clean_text = f"{timestamp}\n{clean_text.strip()}\n" # --- SECURITY FIX: Restrict to ComfyUI Output Directory --- output_dir = folder_paths.get_output_directory() # Remove any leading slashes or drive letters to treat subfolder as relative safe_subfolder = os.path.normpath(output_subfolder).lstrip(os.sep).lstrip("/") # Prevent directory traversal (e.g., "../../") safe_subfolder = safe_subfolder.replace("..", "") base_dir = os.path.join(output_dir, safe_subfolder) # Final safety check: ensure the resulting path is still inside the output directory if os.path.commonpath([os.path.abspath(output_dir), os.path.abspath(base_dir)]) != os.path.abspath(output_dir): print(f"[DebugTextSaver] Security block: attempted to write to {base_dir}") return ("",) os.makedirs(base_dir, exist_ok=True) safe_name = re.sub(r"[^\w\-.]", "_", file_name.strip()) if not safe_name: safe_name = datetime.now().strftime("debug_%Y%m%d_%H%M%S") ext = file_extension.strip().lstrip(".") or "txt" file_path = os.path.join(base_dir, f"{safe_name}.{ext}") # Choose mode mode = "a" if append else "w" # Write (append or overwrite) try: with open(file_path, mode, encoding="utf-8", newline="\n") as f: f.write(clean_text) print(f"[DebugTextSaver] Saved to: {file_path}") except Exception as e: print("[DebugTextSaver] ERROR while writing file!") traceback.print_exc() return ("",) print("[DebugTextSaver] --- run end ---") return (file_path,) NODE_CLASS_MAPPINGS = {"DebugTextSaver": DebugTextSaver} NODE_DISPLAY_NAME_MAPPINGS = {"DebugTextSaver": "Debug Text Saver"}