@@ -10,7 +10,23 @@ set -e # Exit on any error
1010# via --allowedTools flag in CLAUDE_CMD_ARGS, which is the proper approach.
1111# Exporting sandbox variables without a verified sandbox would be misleading.
1212
13- # Source library components
13+ # validate_ralph_dir - Reject dangerous RALPH_DIR values (#79)
14+ # Accepts: ".ralph" (default), empty (becomes .ralph), absolute paths (tests)
15+ # Rejects: paths with ".." (traversal), non-default relative paths
16+ validate_ralph_dir () {
17+ local dir=" $1 "
18+ [[ " $dir " == " .ralph" || -z " $dir " ]] && return 0
19+ [[ " $dir " == * " .." * ]] && { echo " Error: RALPH_DIR must not contain '..': '$dir '" >&2 ; return 1; }
20+ [[ " $dir " != /* ]] && { echo " Error: RALPH_DIR must be '.ralph' or an absolute path, got: '$dir '" >&2 ; return 1; }
21+ return 0
22+ }
23+
24+ # Validate and default RALPH_DIR BEFORE sourcing libraries that depend on it
25+ validate_ralph_dir " ${RALPH_DIR:- } " || exit 1
26+ RALPH_DIR=" ${RALPH_DIR:- .ralph} "
27+ export RALPH_DIR
28+
29+ # Source library components (RALPH_DIR must be set before this point)
1430SCRIPT_DIR=" $( dirname " ${BASH_SOURCE[0]} " ) "
1531source " $SCRIPT_DIR /lib/date_utils.sh"
1632source " $SCRIPT_DIR /lib/timeout_utils.sh"
@@ -20,7 +36,57 @@ source "$SCRIPT_DIR/lib/write_heartbeat.sh"
2036
2137# Configuration
2238# Ralph-specific files live in .ralph/ subfolder
23- RALPH_DIR=" ${RALPH_DIR:- .ralph} "
39+
40+ # Allowlist of known .ralphrc config keys (#76)
41+ # Space-delimited string (avoids declare -A scoping issues when sourced)
42+ RALPHRC_ALLOWED_KEYS=" PLATFORM_DRIVER PROJECT_NAME PROJECT_TYPE MAX_CALLS_PER_HOUR CLAUDE_TIMEOUT_MINUTES CLAUDE_OUTPUT_FORMAT WRITE_TIMEOUT_MINUTES ALLOWED_TOOLS CLAUDE_PERMISSION_MODE PERMISSION_DENIAL_MODE SESSION_CONTINUITY SESSION_EXPIRY_HOURS TASK_SOURCES GITHUB_TASK_LABEL BEADS_FILTER CB_NO_PROGRESS_THRESHOLD CB_SAME_ERROR_THRESHOLD CB_OUTPUT_DECLINE_THRESHOLD CB_READ_ONLY_TIMEOUT_THRESHOLD CB_COOLDOWN_MINUTES CB_AUTO_RESET TEST_COMMAND QUALITY_GATES QUALITY_GATE_MODE QUALITY_GATE_TIMEOUT QUALITY_GATE_ON_COMPLETION_ONLY REVIEW_MODE REVIEW_ENABLED REVIEW_INTERVAL CLAUDE_MIN_VERSION RALPH_VERBOSE PROMPT_FILE FIX_PLAN_FILE AGENT_FILE "
43+
44+ # parse_ralphrc - Safely parse .ralphrc as key=value config (#76)
45+ # Rejects command substitution ($(), backticks). Only sets allowlisted keys.
46+ parse_ralphrc () {
47+ local config_file=" $1 "
48+ local line_num=0
49+ while IFS= read -r line || [[ -n " $line " ]]; do
50+ line_num=$(( line_num + 1 ))
51+ # Strip leading/trailing whitespace
52+ line=" ${line# " ${line%% [![:space:]]* } " } "
53+ line=" ${line% " ${line##* [![:space:]]} " } "
54+ # Skip empty lines and comments
55+ [[ -z " $line " || " $line " == \# * ]] && continue
56+ # Reject command substitution ($() and backticks)
57+ if [[ " $line " == * ' $(' * ]] || [[ " $line " == * ' `' * ]]; then
58+ log_status " WARN" " .ralphrc:$line_num : rejected (command substitution): $line "
59+ continue
60+ fi
61+ # Match KEY=VALUE
62+ if [[ " $line " =~ ^([A-Z_][A-Z0-9_]* )= (.* ) ]]; then
63+ local key=" ${BASH_REMATCH[1]} "
64+ local raw_value=" ${BASH_REMATCH[2]} "
65+ # Check allowlist
66+ if [[ " $RALPHRC_ALLOWED_KEYS " != * " $key " * ]]; then
67+ log_status " WARN" " .ralphrc:$line_num : unknown key ignored: $key "
68+ continue
69+ fi
70+ # Strip outer quotes
71+ local value=" $raw_value "
72+ if [[ " $value " =~ ^\" (.* )\" $ ]]; then
73+ value=" ${BASH_REMATCH[1]} "
74+ elif [[ " $value " =~ ^\' (.* )\' $ ]]; then
75+ value=" ${BASH_REMATCH[1]} "
76+ fi
77+ # Handle ${VAR:-default} and ${VAR:-} (empty default)
78+ if [[ " $value " =~ ^\$\{ ([A-Z_][A-Z0-9_]* ):-([^}]* )\} $ ]]; then
79+ local ref_var=" ${BASH_REMATCH[1]} "
80+ local default_val=" ${BASH_REMATCH[2]} "
81+ local current=" ${! ref_var:- } "
82+ value=" ${current:- $default_val } "
83+ fi
84+ printf -v " $key " ' %s' " $value "
85+ else
86+ log_status " WARN" " .ralphrc:$line_num : skipped (not KEY=VALUE): $line "
87+ fi
88+ done < " $config_file "
89+ }
2490PROMPT_FILE=" $RALPH_DIR /PROMPT.md"
2591LOG_DIR=" $RALPH_DIR /logs"
2692DOCS_DIR=" $RALPH_DIR /docs/generated"
@@ -164,7 +230,7 @@ TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only,
164230# Ralph configuration file
165231# bmalph installs .ralph/.ralphrc. Fall back to a project-root .ralphrc for
166232# older standalone Ralph layouts.
167- RALPHRC_FILE=" ${RALPHRC_FILE :- $ RALPH_DIR/ .ralphrc} "
233+ RALPHRC_FILE=" $RALPH_DIR /.ralphrc"
168234RALPHRC_LOADED=false
169235
170236# Platform driver (set from .ralphrc or environment)
@@ -214,9 +280,8 @@ load_ralphrc() {
214280 return 0
215281 fi
216282
217- # Source config (this may override default values)
218- # shellcheck source=/dev/null
219- source " $config_file "
283+ # Parse config as safe key=value pairs (#76 — no longer sourced as bash)
284+ parse_ralphrc " $config_file "
220285
221286 # Map config variable names to internal names
222287 if [[ -n " ${ALLOWED_TOOLS:- } " ]]; then
@@ -300,6 +365,11 @@ driver_permission_denial_help() {
300365
301366# Source platform driver
302367load_platform_driver () {
368+ # Reject path traversal in PLATFORM_DRIVER (#77)
369+ if [[ " $PLATFORM_DRIVER " =~ [/] ]] || [[ " $PLATFORM_DRIVER " == * " .." * ]]; then
370+ log_status " ERROR" " Invalid PLATFORM_DRIVER (path traversal): $PLATFORM_DRIVER "
371+ return 1
372+ fi
303373 local driver_file=" $SCRIPT_DIR /drivers/${PLATFORM_DRIVER} .sh"
304374 if [[ ! -f " $driver_file " ]]; then
305375 log_status " ERROR" " Platform driver not found: $driver_file "
0 commit comments