Mirrors the security architecture of ComfyUI_DropSendNode v1.1.x to close
the same vulnerability classes. The actual Google OAuth grant path is
unchanged from v1.0.1 (same Flow.from_client_config, same OOB redirect,
same fetch_token call), so existing users' OAuth setup continues to work.
What changed is the wrapper around it. Note: this release has not been
runtime-tested end to end; the security logic is a direct port of the
tested DropSend implementation.
Setup Node refactor:
- Removed google_client_id, google_client_secret, auth_code, and the
service-account JSON from INPUT_TYPES. They no longer enter
PromptServer.history, workflow JSON, PNG metadata, localStorage, or
copy-pasted nodes.
- Added a browser-only modal (web/drivesend_oauth.js) and a same-origin
POST route /drivesend/setup/stash for entry. Hardening: origin check,
live-WebSocket gate, per-IP rate limit, 32 KB body cap, JSON content
type lockdown, 60-second TTL, 32-entry capacity, lock-protected access.
- Added COMFYUI_DRIVESEND_ALLOW_SETUP=1 opt-in gate. Without it the node
refuses to write or clear credentials, blocking remote workflow
submitters from hijacking or wiping the operator's Drive connection.
- Removed plaintext stdout prints of GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
/ GOOGLE_REFRESH_TOKEN / COMFYUI_ENCRYPTION_KEY. Replaced with a
redacted (last-4-chars) banner.
- WebSocket credential delivery is sid-targeted, never broadcast. Refuses
to send when client_id is None and surfaces a clear error. JS handler
verifies client_id matches api.clientId before rendering.
- Service-account JSON is entered via a textarea in the same modal,
base64-encoded into GOOGLE_SERVICE_ACCOUNT_JSON for delivery.
File-system hardening:
- .env and token.json now created via os.open(O_WRONLY|O_CREAT|O_TRUNC,
0o600), race-free, instead of open('w') with default umask (0o644).
- Added safe_paths.py: clamps watch_folder to ComfyUI's output tree
(with COMFYUI_DRIVESEND_ALLOWED_WATCH_PATHS opt-in for additional
roots), rejects symlinks in event paths, validates Drive folder IDs.
- AutoUploader now runs every event path through safe_paths.is_safe_event_path.
Logging:
- Centralized logging in __init__.py with race-free 0o600 creation of
drivesend.log on the root logger. Removed module-level
logging.basicConfig duplicates from monitor_output.py, encrypt_file.py,
drivesend_uploader_node.py.
- Verified: no logger or print call in the codebase emits an access
token, refresh token, client secret, service-account JSON body, or
encryption key value.
.gitignore additions:
- token.json, client_secret.json, service_account.json (Google credential
caches, previously not ignored).
- Editor backups: *.bak, *.backup, *~, *.swp, *.swo, .idea/.
- Encrypted artifacts: *.enc.
- Stray credential material: *.key, *.pem, *.p12, *.pfx.
- .claude/, .pytest_cache/, .coverage, htmlcov/, build artifacts,
Thumbs.db, Desktop.ini.
README rewrite:
- Linear three-step structure (Google Cloud setup, Setup Node, AutoUploader).
- Cloud / Local / Service-Account sections for the Setup Node.
- Threat model section documenting all enforced protections.
- "Setup is disabled" and "Browser delivery refused" troubleshooting.
- No em dashes per project style.
Tests:
- tests/test_security.py with five regression tests covering: send_sync
is never called with sid=None for credential events; payload echoes
client_id for JS-side verification; .env file mode is exactly 0o600;
client_id pattern rejects malformed input; stash entries expire after
TTL. Tests run without google-auth, googleapiclient, or watchdog
installed via ghost modules.
Version: 1.0.1 -> 1.1.0
77 lines
3.0 KiB
Python
77 lines
3.0 KiB
Python
"""
|
|
ComfyUI DriveSend Node
|
|
Automatic Google Drive uploads with optional encryption
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
from pathlib import Path
|
|
from dotenv import load_dotenv
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Logging
|
|
# ---------------------------------------------------------------------------
|
|
# Attach a FileHandler to the root logger so every child logger in this
|
|
# plugin (logging.getLogger(__name__) in our modules) gets its records
|
|
# persisted to drivesend.log alongside ComfyUI's own stdout. We do NOT
|
|
# use logging.basicConfig() here: ComfyUI's main.py configures the root
|
|
# logger before our package is imported, and basicConfig is a no-op
|
|
# once any caller has already configured root.
|
|
#
|
|
# Race-free perms: pre-create the file with 0o600 via os.open() before
|
|
# FileHandler can create it under the default umask (typically 0o644).
|
|
# A follow-up chmod tightens perms if the file already existed with
|
|
# looser bits.
|
|
_LOG_PATH = os.path.join(os.path.dirname(__file__), "drivesend.log")
|
|
try:
|
|
_fd = os.open(_LOG_PATH, os.O_CREAT | os.O_WRONLY | os.O_APPEND, 0o600)
|
|
os.close(_fd)
|
|
except OSError:
|
|
pass
|
|
_root_logger = logging.getLogger()
|
|
if not any(
|
|
isinstance(h, logging.FileHandler)
|
|
and getattr(h, "baseFilename", "") == _LOG_PATH
|
|
for h in _root_logger.handlers
|
|
):
|
|
try:
|
|
_fh = logging.FileHandler(_LOG_PATH)
|
|
_fh.setFormatter(
|
|
logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
|
|
)
|
|
_root_logger.addHandler(_fh)
|
|
if _root_logger.level == logging.NOTSET or _root_logger.level > logging.INFO:
|
|
_root_logger.setLevel(logging.INFO)
|
|
except OSError:
|
|
pass
|
|
try:
|
|
os.chmod(_LOG_PATH, 0o600)
|
|
except OSError:
|
|
pass
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# .env loading
|
|
# ---------------------------------------------------------------------------
|
|
# Load .env from the plugin directory at import time so GOOGLE_CLIENT_ID,
|
|
# GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN, GOOGLE_DRIVE_FOLDER_ID,
|
|
# COMFYUI_ENCRYPTION_KEY, and the uploader's persisted settings are
|
|
# visible in os.environ before any node runs. override=False is
|
|
# intentional. Host-injected env vars (RunPod secrets, Docker env,
|
|
# systemd EnvironmentFile) take precedence over the plugin's local .env.
|
|
_PLUGIN_ENV = Path(__file__).parent / ".env"
|
|
if _PLUGIN_ENV.exists():
|
|
load_dotenv(_PLUGIN_ENV, override=False)
|
|
|
|
from .drivesend_setup_node import NODE_CLASS_MAPPINGS as SETUP_MAPPINGS
|
|
from .drivesend_setup_node import NODE_DISPLAY_NAME_MAPPINGS as SETUP_DISPLAY_MAPPINGS
|
|
from .drivesend_uploader_node import NODE_CLASS_MAPPINGS as UPLOADER_MAPPINGS
|
|
from .drivesend_uploader_node import NODE_DISPLAY_NAME_MAPPINGS as UPLOADER_DISPLAY_MAPPINGS
|
|
|
|
NODE_CLASS_MAPPINGS = {**SETUP_MAPPINGS, **UPLOADER_MAPPINGS}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {**SETUP_DISPLAY_MAPPINGS, **UPLOADER_DISPLAY_MAPPINGS}
|
|
|
|
# Define web directory for JavaScript extensions
|
|
WEB_DIRECTORY = "./web"
|
|
|
|
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
|