fix(security): fix media upload permission denied + symlink protection

- Fix workspace dir ownership in Docker entrypoint: chown dirs not owned
  by goclaw on startup (handles dirs created by root in previous lifecycle)
- Add symlink check on .uploads/ via os.Lstat before file creation to
  prevent symlink-based attacks replacing .uploads with link to sensitive dir
This commit is contained in:
viettranx
2026-04-01 18:11:42 +07:00
parent 41c827c65f
commit d819e08071
2 changed files with 15 additions and 0 deletions
+9
View File
@@ -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"
+6
View File
@@ -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 {