security: disable workspace-local plugin autoload by default (P0 RCE)

ober

37ce5bb987013ec1d90c94bd363d0ef19f55367a

diff --git a/src/jcode/core/plugin.ss b/src/jcode/core/plugin.ss
index 40e00e8..e950f7b 100644
--- a/src/jcode/core/plugin.ss
+++ b/src/jcode/core/plugin.ss
@@ -15,15 +15,30 @@
 
 (def *loaded-plugins* '())
 
+(def (workspace-plugins-allowed?)
+  "Workspace-local plugins (<cwd>/.jcode/plugins) are untrusted: a cloned
+   repository could ship one and obtain silent code execution at startup.
+   They are disabled unless the user explicitly opts in via the
+   JCODE_ALLOW_WORKSPACE_PLUGINS env var or the pluginAllowWorkspace config."
+  (or (let ((v (getenv "JCODE_ALLOW_WORKSPACE_PLUGINS")))
+        (and v (not (string=? v "")) (not (string=? v "0"))))
+      (config-ref "pluginAllowWorkspace")))
+
 (def (plugin-dirs)
   "Return list of directories to scan for plugins."
   (let ((home-dir (path-join (jcode-home) "plugins"))
         (local-dir (path-join (current-directory) ".jcode" "plugins"))
         (cfg-dirs (config-ref "pluginDirs")))
+    (unless (workspace-plugins-allowed?)
+      (when (file-directory? local-dir)
+        (log-warn logger "workspace-plugins-skipped"
+          `((dir . ,local-dir)
+            (hint . "set JCODE_ALLOW_WORKSPACE_PLUGINS=1 to load workspace-local plugins")))))
     (filter file-directory?
       (append
         (if (and cfg-dirs (list? cfg-dirs)) cfg-dirs '())
-        (list home-dir local-dir)))))
+        (list home-dir)
+        (if (workspace-plugins-allowed?) (list local-dir) '())))))
 
 (def (find-plugins)
   "Find all .ss files in plugin directories."
diff --git a/test/security-regression.ss b/test/security-regression.ss
index f262386..7b9f7c9 100644
--- a/test/security-regression.ss
+++ b/test/security-regression.ss
@@ -5,6 +5,7 @@
         (jcode core agent-defs)
         (jcode core debug-repl)
         (jcode core remote-auth)
+        (jcode core plugin)
         (std misc ports)
         (std misc string)
         (std net tcp))
@@ -135,6 +136,39 @@
   (refused? (lambda () (start-jcode-repl! 0 "0.0.0.0"))))
 (putenv "HOME" saved-home)
 
+;; ── (a) workspace plugin auto-load RCE ────────────────────────────────
+;; cwd is the repo root here; a malicious <cwd>/.jcode/plugins/*.ss must NOT
+;; be evaluated at init unless the user explicitly opts in.
+(define plugin-dir (string-append (current-directory) "/.jcode/plugins"))
+(define plugin-evil (string-append plugin-dir "/evil.ss"))
+(define plugin-marker "/tmp/jcode-sec-plugin-marker")
+(define plugin-home "/tmp/jcode-sec-plugin-home")
+(define plugin-saved-home (getenv "HOME"))
+(define plugin-saved-opt (getenv "JCODE_ALLOW_WORKSPACE_PLUGINS"))
+(define plugin-had-dir (file-directory? plugin-dir))
+(system (string-append "mkdir -p '" plugin-home "'"))
+(unless plugin-had-dir (mkdir plugin-dir))
+(call-with-output-file plugin-evil
+  (lambda (p)
+    (display (string-append "(call-with-output-file \"" plugin-marker
+                            "\" (lambda (pp) (display \"pwned\" pp)))") p)))
+(putenv "HOME" plugin-home)
+(putenv "JCODE_ALLOW_WORKSPACE_PLUGINS" "0")
+(when (file-exists? plugin-marker) (delete-file plugin-marker))
+(init-plugins)
+(check "workspace plugin NOT auto-loaded without opt-in (no RCE)"
+  (not (file-exists? plugin-marker)))
+(putenv "JCODE_ALLOW_WORKSPACE_PLUGINS" "1")
+(init-plugins)
+(check "workspace plugin loads with explicit opt-in"
+  (file-exists? plugin-marker))
+(putenv "HOME" plugin-saved-home)
+(putenv "JCODE_ALLOW_WORKSPACE_PLUGINS" (or plugin-saved-opt "0"))
+(when (file-exists? plugin-evil) (delete-file plugin-evil))
+(unless plugin-had-dir (system (string-append "rmdir '" plugin-dir "'")))
+(when (file-exists? plugin-marker) (delete-file plugin-marker))
+(system (string-append "rm -rf '" plugin-home "'"))
+
 (when (> failures 0)
   (error 'security-regression (format "~a security regression test(s) failed" failures)))
 (printf "Security regressions passed~n")