#!/bin/bash # check-cwe134.sh - Static analysis for CWE-134 format string vulnerabilities # # This script detects potentially unsafe usage of format string functions # where user-controlled data may be passed without "%s" wrapper. # # Checks performed: # 1. Direct variable passed as format string: cons_show(var); # 2. Variable passed as format arg through multi-arg wrappers: # win_println(win, theme, ch, var); (no format specifiers in var position) # 3. GString->str passed as format argument without "%s" # 4. Presence of __attribute__((format)) on printf-like wrapper declarations # 5. Format string mismatch: more arguments than format specifiers # # Usage: ./check-cwe134.sh [directory] set -e DIR="${1:-src}" echo "=== CWE-134 Format String Vulnerability Check ===" echo "Scanning: $DIR" echo "" # Functions that accept format strings (direct: first arg is format) DIRECT_FORMAT_FUNCS="cons_show|cons_debug|cons_show_error|log_info|log_error|log_warning|log_debug" # Functions where format string is not the first arg (multi-arg wrappers) # win_print(win, theme, char, FORMAT, ...) # win_println(win, theme, char, FORMAT, ...) # win_println_indent(win, pad, FORMAT, ...) # win_append(win, theme, FORMAT, ...) # win_appendln(win, theme, FORMAT, ...) # win_append_highlight(win, theme, FORMAT, ...) # win_appendln_highlight(win, theme, FORMAT, ...) # win_command_exec_error(win, cmd, FORMAT, ...) MULTI_ARG_FORMAT_FUNCS="win_print|win_println|win_println_indent|win_append|win_appendln|win_append_highlight|win_appendln_highlight|win_command_exec_error" ALL_FORMAT_FUNCS="$DIRECT_FORMAT_FUNCS|$MULTI_ARG_FORMAT_FUNCS" ERRORS=0 # --------------------------------------------------------------------------- # Check 1: Direct format functions called with single variable (no format string) # --------------------------------------------------------------------------- echo "Check 1: Direct variable as format string..." echo "" # Pattern: func(identifier) or func(identifier->member) or func(identifier[index]) RESULTS=$(grep -rn --include="*.c" -P "($DIRECT_FORMAT_FUNCS)\s*\(\s*[a-zA-Z_][a-zA-Z0-9_]*(\s*->\s*\w+|\s*\[\s*\w+\s*\])?\s*\)\s*;" "$DIR" 2>/dev/null || true) # Filter out function definitions, declarations, and safe api_* wrappers RESULTS=$(echo "$RESULTS" | grep -v "const char\|void \|^[^:]*:[0-9]*:[a-z_]*(\|api_cons_show\|api_log_" || true) if [ -n "$RESULTS" ]; then echo "❌ POTENTIAL CWE-134 VULNERABILITIES FOUND (direct format):" echo "" echo "$RESULTS" echo "" ERRORS=$((ERRORS + $(echo "$RESULTS" | wc -l))) else echo "✅ No direct format string issues found." fi # --------------------------------------------------------------------------- # Check 2: Multi-arg wrappers where format position has a variable (not a string literal) # --------------------------------------------------------------------------- echo "" echo "Check 2: Multi-arg wrappers with variable as format argument..." echo "" # For win_print(win, theme, char, FORMAT, ...) and win_println(win, theme, char, FORMAT, ...): # FORMAT is the 4th argument MULTI4_RESULTS=$(grep -rn --include="*.c" -P "(win_print|win_println)\s*\(" "$DIR" 2>/dev/null | \ grep -v "win_println_indent\|win_println_va" | \ perl -ne ' if (/((win_print|win_println)\s*\()/) { my $line = $_; my $call_start = index($line, $1) + length($1); my $rest = substr($line, $call_start); my $depth = 0; my $commas = 0; my $i = 0; for ($i = 0; $i < length($rest) && $commas < 3; $i++) { my $c = substr($rest, $i, 1); if ($c eq "(") { $depth++; } elsif ($c eq ")") { $depth--; last if $depth < 0; } elsif ($c eq "," && $depth == 0) { $commas++; } } if ($commas == 3) { my $fmt_arg = substr($rest, $i); $fmt_arg =~ s/^\s+//; if ($fmt_arg =~ /^[a-zA-Z_]/) { print $line; } } } ' || true) MULTI4_RESULTS=$(echo "$MULTI4_RESULTS" | grep -v "^$\|const char.*message\|void " || true) # For win_command_exec_error(win, cmd, FORMAT, ...): # FORMAT is the 3rd argument MULTI_CMD_RESULTS=$(grep -rn --include="*.c" -P "win_command_exec_error\s*\(" "$DIR" 2>/dev/null | \ perl -ne ' if (/(win_command_exec_error\s*\()/) { my $line = $_; my $call_start = index($line, $1) + length($1); my $rest = substr($line, $call_start); my $depth = 0; my $commas = 0; my $i = 0; for ($i = 0; $i < length($rest) && $commas < 2; $i++) { my $c = substr($rest, $i, 1); if ($c eq "(") { $depth++; } elsif ($c eq ")") { $depth--; last if $depth < 0; } elsif ($c eq "," && $depth == 0) { $commas++; } } if ($commas == 2) { my $fmt_arg = substr($rest, $i); $fmt_arg =~ s/^\s+//; if ($fmt_arg =~ /^[a-zA-Z_]/) { print $line; } } } ' || true) MULTI_CMD_RESULTS=$(echo "$MULTI_CMD_RESULTS" | grep -v "^$\|const char.*error\|void \|ProfWin\*.*const char\*.*const char\*.*\.\.\." || true) # For 3-arg format functions: win_println_indent(win, pad, FORMAT, ...) # win_append(win, theme, FORMAT, ...) etc. MULTI3_RESULTS=$(grep -rn --include="*.c" -P "(win_println_indent|win_append|win_appendln|win_append_highlight|win_appendln_highlight)\s*\([^,]+,[^,]+,\s*(?!\")\s*[a-zA-Z_][a-zA-Z0-9_]*(\s*->\s*\w+|\s*\[\s*[^\]]+\])?\s*\)\s*;" "$DIR" 2>/dev/null || true) MULTI3_RESULTS=$(echo "$MULTI3_RESULTS" | grep -v "^$\|const char.*message\|void " || true) MULTI_RESULTS="" if [ -n "$MULTI4_RESULTS" ]; then MULTI_RESULTS="$MULTI4_RESULTS" fi if [ -n "$MULTI3_RESULTS" ]; then if [ -n "$MULTI_RESULTS" ]; then MULTI_RESULTS="$MULTI_RESULTS $MULTI3_RESULTS" else MULTI_RESULTS="$MULTI3_RESULTS" fi fi if [ -n "$MULTI_CMD_RESULTS" ]; then if [ -n "$MULTI_RESULTS" ]; then MULTI_RESULTS="$MULTI_RESULTS $MULTI_CMD_RESULTS" else MULTI_RESULTS="$MULTI_CMD_RESULTS" fi fi if [ -n "$MULTI_RESULTS" ]; then echo "❌ POTENTIAL CWE-134 VULNERABILITIES FOUND (multi-arg wrappers):" echo "" echo "$MULTI_RESULTS" echo "" ERRORS=$((ERRORS + $(echo "$MULTI_RESULTS" | wc -l))) else echo "✅ No multi-arg format string issues found." fi # --------------------------------------------------------------------------- # Check 3: GString->str passed directly as format argument # --------------------------------------------------------------------------- echo "" echo "Check 3: GString->str passed to format functions..." GSTRING_RESULTS=$(grep -rn --include="*.c" -P "($ALL_FORMAT_FUNCS)\s*\([^)]*->str\s*\)" "$DIR" 2>/dev/null | grep -v '"%s"' || true) if [ -n "$GSTRING_RESULTS" ]; then echo "⚠️ GString->str passed without \"%s\" (review manually):" echo "" echo "$GSTRING_RESULTS" echo "" fi # --------------------------------------------------------------------------- # Check 4: Verify __attribute__((format)) on printf-like wrappers # --------------------------------------------------------------------------- echo "" echo "Check 4: Verifying __attribute__((format)) annotations..." echo "" ATTR_ERRORS=0 # Functions that MUST have format attributes (declared in headers) REQUIRED_ATTRIBUTED=( "cons_show" "cons_debug" "cons_show_error" "cons_show_padded" "log_debug" "log_info" "log_warning" "log_error" "win_print" "win_println" "win_println_indent" "win_println_va" "win_append" "win_appendln" "win_append_highlight" "win_appendln_highlight" "win_command_exec_error" ) for func in "${REQUIRED_ATTRIBUTED[@]}"; do # Check if the function declaration in headers has a preceding format attribute HAS_ATTR=$(grep -B1 --include="*.h" -rn "void ${func}\s*(" "$DIR" 2>/dev/null | grep -E -c "format\(printf|G_GNUC_PRINTF" || true) if [ "$HAS_ATTR" -eq 0 ]; then echo "❌ Missing __attribute__((format(printf, ...))) for: $func" ATTR_ERRORS=$((ATTR_ERRORS + 1)) fi done if [ "$ATTR_ERRORS" -eq 0 ]; then echo "✅ All printf-like wrappers have format attributes." else echo "" echo "⚠️ $ATTR_ERRORS function(s) missing format attributes." echo " Add __attribute__((format(printf, N, M))) before the declaration." ERRORS=$((ERRORS + ATTR_ERRORS)) fi # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- echo "" echo "=== Summary ===" echo "Critical issues: $ERRORS" if [ "$ERRORS" -gt 0 ]; then echo "" echo "Fix by adding \"%s\" format specifier:" echo " BAD: cons_show(variable);" echo " GOOD: cons_show(\"%s\", variable);" echo "" echo " BAD: win_println(win, theme, ch, variable);" echo " GOOD: win_println(win, theme, ch, \"%s\", variable);" exit 1 fi exit 0