Skip to content

Instantly share code, notes, and snippets.

@tluyben
Created December 4, 2025 00:57
Show Gist options
  • Select an option

  • Save tluyben/f8051d0557025add079e377f1395182f to your computer and use it in GitHub Desktop.

Select an option

Save tluyben/f8051d0557025add079e377f1395182f to your computer and use it in GitHub Desktop.
#!/bin/bash
#
# CVE Fix Script for GHSA-9qr9-h5gf-34mp
# Scans directories for vulnerable Next.js/React versions and patches them
#
# Usage: ./fix-cve.sh <directory> [FIX]
# <directory> - Parent directory to scan for Next.js projects
# FIX - Optional: Actually apply fixes (default is dry-run)
#
# Fixed versions:
# React: 19.0.1, 19.1.2, 19.2.1
# Next.js: 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7
#
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[OK]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_action() {
echo -e "${CYAN}[ACTION]${NC} $1"
}
log_dry() {
echo -e "${YELLOW}[DRY-RUN]${NC} $1"
}
# Fixed versions mapping (minor version -> fixed patch version)
declare -A NEXTJS_FIXED_VERSIONS=(
["15.0"]="15.0.5"
["15.1"]="15.1.9"
["15.2"]="15.2.6"
["15.3"]="15.3.6"
["15.4"]="15.4.8"
["15.5"]="15.5.7"
["16.0"]="16.0.7"
)
# React versions corresponding to Next.js minor versions
declare -A REACT_FIXED_VERSIONS=(
["15.0"]="19.0.1"
["15.1"]="19.0.1"
["15.2"]="19.0.1"
["15.3"]="19.0.1"
["15.4"]="19.0.1"
["15.5"]="19.1.2"
["16.0"]="19.2.1"
)
# Parse command line arguments
TARGET_DIR="$1"
FIX_MODE="$2"
if [[ -z "$TARGET_DIR" ]]; then
echo "Usage: $0 <directory> [FIX]"
echo ""
echo " <directory> Parent directory to scan for Next.js projects"
echo " FIX Optional: Actually apply fixes (default is dry-run)"
echo ""
echo "Example:"
echo " $0 /path/to/projects # Dry-run mode"
echo " $0 /path/to/projects FIX # Actually fix vulnerabilities"
exit 1
fi
if [[ ! -d "$TARGET_DIR" ]]; then
log_error "Directory does not exist: $TARGET_DIR"
exit 1
fi
DRY_RUN=true
if [[ "$FIX_MODE" == "FIX" ]]; then
DRY_RUN=false
log_warn "FIX MODE ENABLED - Changes will be applied!"
else
log_info "DRY-RUN MODE - No changes will be made"
fi
echo ""
echo "========================================"
echo " CVE Fix: GHSA-9qr9-h5gf-34mp"
echo " Scanning: $TARGET_DIR"
echo " Mode: $(if $DRY_RUN; then echo 'DRY-RUN'; else echo 'FIX'; fi)"
echo "========================================"
echo ""
# Extract version from package.json value (handles ^, ~, and exact versions)
extract_version() {
local version_string="$1"
# Remove ^, ~, >=, etc. prefixes
echo "$version_string" | sed 's/^[\^~>=<]*//g'
}
# Get minor version (e.g., "15.4" from "15.4.3")
get_minor_version() {
local version="$1"
echo "$version" | cut -d'.' -f1,2
}
# Compare versions: returns 0 if $1 < $2, 1 otherwise
version_lt() {
local v1="$1"
local v2="$2"
# Use sort -V for version comparison
if [[ "$(printf '%s\n' "$v1" "$v2" | sort -V | head -n1)" == "$v1" && "$v1" != "$v2" ]]; then
return 0 # v1 < v2
else
return 1 # v1 >= v2
fi
}
# Check if a Next.js version is vulnerable
is_vulnerable() {
local version="$1"
local minor_version=$(get_minor_version "$version")
local fixed_version="${NEXTJS_FIXED_VERSIONS[$minor_version]}"
if [[ -z "$fixed_version" ]]; then
# Unknown minor version - can't determine
return 1
fi
if version_lt "$version" "$fixed_version"; then
return 0 # Vulnerable
else
return 1 # Not vulnerable
fi
}
# Update package.json with new versions
update_package_json() {
local pkg_file="$1"
local next_version="$2"
local react_version="$3"
local eslint_next_version="$4"
log_action "Updating $pkg_file"
# Create backup
cp "$pkg_file" "$pkg_file.bak"
log_info " Created backup: $pkg_file.bak"
# Update next version (handle both exact and ^ versions)
sed -i -E "s/\"next\": *\"[\^~]?[0-9]+\.[0-9]+\.[0-9]+\"/\"next\": \"$next_version\"/" "$pkg_file"
log_info " Updated next to $next_version"
# Update react version
sed -i -E "s/\"react\": *\"[\^~]?[0-9]+\.[0-9]+\.[0-9]+\"/\"react\": \"$react_version\"/" "$pkg_file"
log_info " Updated react to $react_version"
# Update react-dom version
sed -i -E "s/\"react-dom\": *\"[\^~]?[0-9]+\.[0-9]+\.[0-9]+\"/\"react-dom\": \"$react_version\"/" "$pkg_file"
log_info " Updated react-dom to $react_version"
# Update eslint-config-next version
sed -i -E "s/\"eslint-config-next\": *\"[\^~]?[0-9]+\.[0-9]+\.[0-9]+\"/\"eslint-config-next\": \"$eslint_next_version\"/" "$pkg_file"
log_info " Updated eslint-config-next to $eslint_next_version"
}
# Run npm install
run_npm_install() {
local project_dir="$1"
log_action "Running npm install in $project_dir"
# Check if .npmrc exists with legacy-peer-deps
if [[ -f "$project_dir/.npmrc" ]] && grep -q "legacy-peer-deps" "$project_dir/.npmrc"; then
log_info " Using existing .npmrc with legacy-peer-deps"
(cd "$project_dir" && npm install 2>&1) | while read line; do
echo " [npm] $line"
done
else
(cd "$project_dir" && npm install 2>&1) | while read line; do
echo " [npm] $line"
done
fi
if [[ ${PIPESTATUS[0]} -eq 0 ]]; then
log_success "npm install completed"
return 0
else
log_error "npm install failed"
return 1
fi
}
# Run npm run check
run_npm_check() {
local project_dir="$1"
log_action "Running npm run check in $project_dir"
local output
output=$(cd "$project_dir" && npm run check 2>&1)
local exit_code=$?
echo "$output" | while read line; do
echo " [tsc] $line"
done
if [[ $exit_code -eq 0 ]]; then
log_success "TypeScript check passed"
return 0
else
log_error "TypeScript check failed!"
return 1
fi
}
# Counters
TOTAL_SCANNED=0
TOTAL_NEXTJS=0
TOTAL_VULNERABLE=0
TOTAL_FIXED=0
TOTAL_ALREADY_PATCHED=0
TOTAL_ERRORS=0
# Scan subdirectories
log_info "Scanning subdirectories..."
echo ""
for subdir in "$TARGET_DIR"/*/; do
# Skip if not a directory
[[ ! -d "$subdir" ]] && continue
TOTAL_SCANNED=$((TOTAL_SCANNED + 1))
dir_name=$(basename "$subdir")
pkg_file="$subdir/package.json"
log_info "Checking: $dir_name"
# Check if package.json exists
if [[ ! -f "$pkg_file" ]]; then
log_info " No package.json found, skipping"
echo ""
continue
fi
# Check if it's a Next.js project
if ! grep -q '"next"' "$pkg_file"; then
log_info " Not a Next.js project, skipping"
echo ""
continue
fi
TOTAL_NEXTJS=$((TOTAL_NEXTJS + 1))
log_info " Found Next.js project"
# Extract current versions
current_next_raw=$(grep -oP '"next": *"\K[^"]+' "$pkg_file" | head -1)
current_next=$(extract_version "$current_next_raw")
current_react_raw=$(grep -oP '"react": *"\K[^"]+' "$pkg_file" | head -1)
current_react=$(extract_version "$current_react_raw")
current_eslint_raw=$(grep -oP '"eslint-config-next": *"\K[^"]+' "$pkg_file" 2>/dev/null | head -1)
current_eslint=$(extract_version "$current_eslint_raw")
log_info " Current versions:"
log_info " next: $current_next (raw: $current_next_raw)"
log_info " react: $current_react (raw: $current_react_raw)"
log_info " eslint-config-next: ${current_eslint:-'not found'} (raw: ${current_eslint_raw:-'not found'})"
# Get minor version and check if we have a fix for it
minor_version=$(get_minor_version "$current_next")
fixed_next="${NEXTJS_FIXED_VERSIONS[$minor_version]}"
fixed_react="${REACT_FIXED_VERSIONS[$minor_version]}"
if [[ -z "$fixed_next" ]]; then
log_warn " Unknown Next.js minor version: $minor_version - cannot determine vulnerability"
echo ""
continue
fi
log_info " Minor version: $minor_version"
log_info " Fixed versions for this minor: next=$fixed_next, react=$fixed_react"
# Check if vulnerable
if is_vulnerable "$current_next"; then
TOTAL_VULNERABLE=$((TOTAL_VULNERABLE + 1))
log_warn " VULNERABLE! $current_next < $fixed_next"
if $DRY_RUN; then
log_dry " Would update:"
log_dry " next: $current_next -> $fixed_next"
log_dry " react: $current_react -> $fixed_react"
log_dry " react-dom: $current_react -> $fixed_react"
log_dry " eslint-config-next: ${current_eslint:-'?'} -> $fixed_next"
log_dry " Would run: npm install"
log_dry " Would run: npm run check"
else
log_action "Applying fix..."
# Update package.json
update_package_json "$pkg_file" "$fixed_next" "$fixed_react" "$fixed_next"
# Run npm install
if run_npm_install "$subdir"; then
# Run npm check
if run_npm_check "$subdir"; then
TOTAL_FIXED=$((TOTAL_FIXED + 1))
log_success "Successfully fixed $dir_name"
else
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
log_error "TypeScript check failed for $dir_name"
log_warn "Backup available at: $pkg_file.bak"
fi
else
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
log_error "npm install failed for $dir_name"
log_warn "Backup available at: $pkg_file.bak"
fi
fi
else
TOTAL_ALREADY_PATCHED=$((TOTAL_ALREADY_PATCHED + 1))
log_success " Already patched ($current_next >= $fixed_next)"
fi
echo ""
done
# Summary
echo ""
echo "========================================"
echo " SUMMARY"
echo "========================================"
echo ""
log_info "Directories scanned: $TOTAL_SCANNED"
log_info "Next.js projects found: $TOTAL_NEXTJS"
log_info "Vulnerable projects: $TOTAL_VULNERABLE"
log_info "Already patched: $TOTAL_ALREADY_PATCHED"
if $DRY_RUN; then
echo ""
log_warn "DRY-RUN MODE - No changes were made"
if [[ $TOTAL_VULNERABLE -gt 0 ]]; then
log_info "Run with FIX parameter to apply fixes:"
log_info " $0 $TARGET_DIR FIX"
fi
else
log_success "Successfully fixed: $TOTAL_FIXED"
if [[ $TOTAL_ERRORS -gt 0 ]]; then
log_error "Errors encountered: $TOTAL_ERRORS"
fi
fi
echo ""
echo "========================================"
# Exit with error code if there were errors
if [[ $TOTAL_ERRORS -gt 0 ]]; then
exit 1
fi
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment