diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 6c30180e..1eaa8896 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -19,6 +19,15 @@ if [ "$(id -u)" = "0" ] && [ -d "$RUNTIME_DIR" ]; then chown -R goclaw:goclaw "$RUNTIME_DIR/pip" "$RUNTIME_DIR/npm-global" "$RUNTIME_DIR/pip-cache" 2>/dev/null || true fi +# Fix workspace directory ownership: handle dirs created by root in previous +# container lifecycle or via manual docker exec. +# Security: -type d = real directories only (not symlinks). +# find default -P mode = never follow symlinks. -maxdepth 5 limits traversal. +if [ "$(id -u)" = "0" ] && [ -d /app/workspace ]; then + find /app/workspace -maxdepth 5 -type d -not -user goclaw \ + -exec chown goclaw:goclaw {} + 2>/dev/null || true +fi + # Python: allow agent to pip install to writable target dir export PYTHONPATH="$RUNTIME_DIR/pip:${PYTHONPATH:-}" export PIP_TARGET="$RUNTIME_DIR/pip" diff --git a/internal/agent/media.go b/internal/agent/media.go index a0c68e71..76003e56 100644 --- a/internal/agent/media.go +++ b/internal/agent/media.go @@ -70,6 +70,12 @@ func (l *Loop) persistMedia(sessionKey string, files []bus.MediaFile, workspace slog.Warn("media: failed to create .uploads dir", "dir", uploadsDir, "error", err) return nil } + // Verify .uploads is a real directory (not symlink) to prevent symlink-based attacks. + // os.Lstat does NOT follow symlinks — rejects if attacker replaced .uploads with a symlink. + if fi, err := os.Lstat(uploadsDir); err == nil && fi.Mode()&os.ModeSymlink != 0 { + slog.Warn("media: .uploads is a symlink, refusing to use", "dir", uploadsDir) + return nil + } var refs []providers.MediaRef for _, f := range files {