loki-mode 7.78.0 → 7.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -115,6 +115,88 @@ log_error() {
115
115
  echo -e "${RED}[SANDBOX]${NC} $1" >&2
116
116
  }
117
117
 
118
+ #===============================================================================
119
+ # A5: ALLOWED_PATHS + BLOCKED_COMMANDS enforcement (sandbox-local)
120
+ #
121
+ # sandbox.sh runs as its own process (set -euo pipefail) and does NOT source
122
+ # run.sh, so the run.sh helpers are out of scope. These are self-contained
123
+ # mirrors that read the same env vars (LOKI_ALLOWED_PATHS / LOKI_BLOCKED_COMMANDS)
124
+ # so the sandbox -- the real security boundary -- enforces them at the point
125
+ # where run.sh/sandbox.sh actually controls the surface: which host paths get
126
+ # bind-mounted, and the operator-supplied `loki sandbox run <cmd>` argv.
127
+ #
128
+ # HONEST SCOPE: this does NOT restrict provider-driven agent writes inside the
129
+ # container (those are contained by cap-drop/seccomp/read-only mounts, not a
130
+ # path allowlist). It is defense-in-depth on the surfaces run.sh owns.
131
+ #===============================================================================
132
+
133
+ # Return 0 if target is within LOKI_ALLOWED_PATHS (or the allowlist is unset/
134
+ # empty -> no restriction). realpath -m collapses ../ traversal before the
135
+ # prefix check, so allowed/../../etc cannot slip past.
136
+ _sandbox_path_within_allowed() {
137
+ local target="$1"
138
+ local allow="${LOKI_ALLOWED_PATHS:-}"
139
+ [ -z "$allow" ] && return 0
140
+ [ -z "$target" ] && return 1
141
+
142
+ local norm_target
143
+ norm_target="$(realpath -m -- "$target" 2>/dev/null || true)"
144
+ if [ -z "$norm_target" ]; then
145
+ norm_target="$(LOKI_AP_T="$target" python3 -c 'import os; print(os.path.realpath(os.environ["LOKI_AP_T"]))' 2>/dev/null || true)"
146
+ fi
147
+ [ -z "$norm_target" ] && return 1
148
+
149
+ local entry norm_entry
150
+ IFS=',' read -ra _AP_ARRAY <<< "$allow"
151
+ for entry in "${_AP_ARRAY[@]}"; do
152
+ entry="${entry#"${entry%%[![:space:]]*}"}"
153
+ entry="${entry%"${entry##*[![:space:]]}"}"
154
+ [ -z "$entry" ] && continue
155
+ # Tilde-expand allowlist entries so "~/proj" matches a resolved $HOME path.
156
+ # SC2088 disabled intentionally: we are pattern-MATCHING a literal "~/" at
157
+ # the start of a config-supplied string (no expansion wanted here), then
158
+ # expanding it ourselves via $HOME. The directive sits in front of the whole
159
+ # if-block (it cannot precede an elif branch -- SC1123).
160
+ # shellcheck disable=SC2088
161
+ if [[ "$entry" == "~/"* ]]; then
162
+ entry="$HOME/${entry#\~/}"
163
+ elif [[ "$entry" == "~" ]]; then
164
+ entry="$HOME"
165
+ fi
166
+ norm_entry="$(realpath -m -- "$entry" 2>/dev/null || true)"
167
+ if [ -z "$norm_entry" ]; then
168
+ norm_entry="$(LOKI_AP_E="$entry" python3 -c 'import os; print(os.path.realpath(os.environ["LOKI_AP_E"]))' 2>/dev/null || true)"
169
+ fi
170
+ [ -z "$norm_entry" ] && continue
171
+ if [ "$norm_target" = "$norm_entry" ] || [ "${norm_target#"${norm_entry}/"}" != "$norm_target" ]; then
172
+ return 0
173
+ fi
174
+ done
175
+ return 1
176
+ }
177
+
178
+ # Return 1 (block) if the command string contains a blocked pattern. Mirrors
179
+ # run.sh check_command_allowed; default list matches run.sh's BLOCKED_COMMANDS.
180
+ _sandbox_command_allowed() {
181
+ local command="$1"
182
+ # NOTE: the default list is assigned to a separate var first. It cannot be an
183
+ # inline ${VAR:-default} default because the default value contains a literal
184
+ # "}" (the fork-bomb ":(){ :|:& };:"), which would prematurely terminate the
185
+ # ${...} parameter expansion and corrupt the list.
186
+ local _default_blocked='rm -rf /,dd if=,mkfs,:(){ :|:& };:'
187
+ local blocked_list="${LOKI_BLOCKED_COMMANDS:-$_default_blocked}"
188
+ local blocked
189
+ IFS=',' read -ra _BC_ARRAY <<< "$blocked_list"
190
+ for blocked in "${_BC_ARRAY[@]}"; do
191
+ [ -z "$blocked" ] && continue
192
+ if [[ "$command" == *"$blocked"* ]]; then
193
+ log_error "SECURITY: blocked dangerous command (matched '$blocked'): $command"
194
+ return 1
195
+ fi
196
+ done
197
+ return 0
198
+ }
199
+
118
200
  # Check if a port is available
119
201
  check_port_available() {
120
202
  local port="$1"
@@ -1156,6 +1238,15 @@ start_sandbox() {
1156
1238
  c_host="$HOME"
1157
1239
  fi
1158
1240
  if [[ -e "$c_host" ]]; then
1241
+ # A5: when LOKI_ALLOWED_PATHS is set, reject a custom mount
1242
+ # whose host path is outside the allowlist. This is a write
1243
+ # surface run.sh/sandbox.sh genuinely controls (we decide what
1244
+ # to bind into the container), so the allowlist is enforced
1245
+ # here rather than only documented.
1246
+ if ! _sandbox_path_within_allowed "$c_host"; then
1247
+ log_error "Custom mount host path is outside LOKI_ALLOWED_PATHS, refusing to mount: $c_host"
1248
+ return 1
1249
+ fi
1159
1250
  docker_args+=("--volume" "${c_host}:${c_container}:${c_mode}")
1160
1251
  log_info " Custom mount: $c_host -> $c_container ($c_mode)"
1161
1252
  else
@@ -1351,6 +1442,13 @@ sandbox_run() {
1351
1442
  return 1
1352
1443
  fi
1353
1444
 
1445
+ # A5: defense-in-depth on the operator-supplied command. The container is the
1446
+ # real boundary, but an obviously-destructive command is blocked here before
1447
+ # it is dispatched into the sandbox (honors LOKI_BLOCKED_COMMANDS).
1448
+ if ! _sandbox_command_allowed "$cmd"; then
1449
+ return 1
1450
+ fi
1451
+
1354
1452
  check_docker
1355
1453
 
1356
1454
  if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then