[object Object]

← back to Cncp Mcp

security: apply-pm2-caps now whitelists env (was leaking GODADDY_API_KEY etc into ecosystem files)

b5c84595d23e6110b0559b8158995290f5d3017b · 2026-05-11 09:44:46 -0700 · Steve Abrams

Files touched

Diff

commit b5c84595d23e6110b0559b8158995290f5d3017b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 11 09:44:46 2026 -0700

    security: apply-pm2-caps now whitelists env (was leaking GODADDY_API_KEY etc into ecosystem files)
---
 scripts/apply-pm2-caps.sh | 39 +++++++++++++++++++++++++++++----------
 1 file changed, 29 insertions(+), 10 deletions(-)

diff --git a/scripts/apply-pm2-caps.sh b/scripts/apply-pm2-caps.sh
index 8d11546..8238ce8 100644
--- a/scripts/apply-pm2-caps.sh
+++ b/scripts/apply-pm2-caps.sh
@@ -113,22 +113,41 @@ for t in todo:
         logmsg(f"already-has-ecosystem {t['name']}")
         already += 1
         continue
-    # Build the script body — preserve ALL user env vars from current pm2 state.
-    # pm2_env mixes internal keys (pm_*, PM2_*, status, etc.) with user env;
-    # we whitelist by skipping pm2-internal prefixes.
+    # Build the script body — STRICT WHITELIST env approach.
+    # We do NOT capture arbitrary env vars from pm2_env because that pool
+    # contains real secrets (API keys, DB passwords) inherited from the shell
+    # that started pm2. Writing those into ecosystem.config.cjs and committing
+    # to git is a security leak. Instead: capture ONLY known-safe keys.
     proc = [p for p in procs if p.get('name') == t['name']][0]
     full_env = proc.get('pm2_env', {})
-    SKIP_PREFIXES = ('pm_', 'pm2_', 'PM2_', 'PWD', 'OLDPWD', '_', 'SHLVL')
-    SKIP_KEYS = {'instance_var','restart_time','unstable_restarts','created_at','axm_actions','axm_monitor','axm_options','axm_dynamic','axm_metrics','exec_interpreter','watch','env','env_diff','status','windowsHide','vizion_running','instances','exec_mode','autorestart','automation','treekill','username','versioning','version','node_args','source_map_support','exit_code','pmx','filter_env','disable_trace','merge_logs','vizion','autostart','windows_hide','watch_options','APP_PATH','HOME','PATH','SHELL','USER','LOGNAME','TERM','LANG','LC_ALL','TMPDIR','XPC_FLAGS','XPC_SERVICE_NAME','__CFBundleIdentifier','SSH_AUTH_SOCK','SSH_CLIENT','SSH_CONNECTION','SSH_TTY','TERM_PROGRAM','TERM_PROGRAM_VERSION','TERM_SESSION_ID','ITERM_PROFILE','ITERM_SESSION_ID','MAIL','OLDPWD','PYENV_SHELL','LSCOLORS','LS_COLORS','COLORTERM','HOMEBREW_PREFIX','HOMEBREW_CELLAR','HOMEBREW_REPOSITORY','MANPATH','INFOPATH','EDITOR'}
+
+    SAFE_KEYS = {'PORT', 'port', 'NODE_ENV', 'HOST', 'BIND', 'HOSTNAME',
+                 'FLASK_RUN_HOST', 'UVICORN_HOST', 'GUNICORN_BIND', 'VITE_HOST',
+                 'NEXT_TELEMETRY_DISABLED'}
+    # Any *_URL is allowed only if it doesn't contain '@' (which signals creds in URL)
+    SECRET_PATTERNS = ('_KEY', '_SECRET', '_TOKEN', '_PASSWORD', '_PASS',
+                       '_AUTH', '_CREDENTIAL', 'API_', 'PRIVATE_', 'COOKIE',
+                       'SESSION_SECRET', 'JWT')
+
     user_env = {}
     for k, v in full_env.items():
         if not isinstance(v, (str, int, bool)): continue
-        if any(k.startswith(p) for p in SKIP_PREFIXES): continue
-        if k in SKIP_KEYS: continue
-        # Skip giant string values that aren't real config
         sv = str(v)
         if len(sv) > 500: continue
-        user_env[k] = v
+        # Direct allowlist
+        if k in SAFE_KEYS:
+            user_env[k] = v
+            continue
+        # *_URL allowed only if no embedded creds
+        if k.endswith('_URL') and '@' not in sv:
+            # Also exclude obviously-secret URL keys
+            if any(p in k.upper() for p in SECRET_PATTERNS): continue
+            user_env[k] = v
+            continue
+        # Everything else: SKIP. Secrets, pm2 internals, macOS env, claude-session
+        # state, etc. all stay out of the ecosystem file.
+        continue
+
     env_lines = []
     for k, v in user_env.items():
         if isinstance(v, bool):
@@ -136,7 +155,7 @@ for t in todo:
         elif isinstance(v, int):
             env_lines.append(f"      {k}: {v},")
         else:
-            esc = str(v).replace('\\', '\\\\').replace('"', '\\"')
+            esc = str(v).replace("\\", "\\\\").replace('"', '\\"')
             env_lines.append(f'      {k}: "{esc}",')
     env_block = "\n".join(env_lines) + "\n" if env_lines else ""
     script_basename = os.path.basename(t['script'])

← e6e3182 load-aware dispatch: run-on-mac1 routes Mac2↔Mac1 by load +  ·  back to Cncp Mcp  ·  security: apply-pm2-caps now uses mktemp + trap-unlink for p c65b01b →