#!/bin/bash # ============================================================================= # pre-receive-server.sh — Gitea 서버용 보안 게이트 # ============================================================================= # 설치: Gitea 웹 관리자 → 저장소 → Settings → Git Hooks → pre-receive # 또는: cp pre-receive-server.sh $REPO_PATH/custom/hooks/pre-receive # # 동작: push 시 변경 내용을 regex 검사, 위반 시 push 차단 # bypass: 커밋 메시지에 [SECURITY-BYPASS: 사유] 포함 시 통과 + 로그 # ============================================================================= set -uo pipefail # --- 설정 --- BYPASS_LOG="/data/gitea/security-bypass.log" ALLOWED_BYPASS_EMAILS="ahn@hyungi.net hyungi@technicalkorea.net" MEDIUM_THRESHOLD=5 ZERO_REV="0000000000000000000000000000000000000000" # --- 검출 규칙 (security-scan.sh와 동일, 자체 내장) --- RULES=( 'SECRET_HARDCODE|CRITICAL|비밀정보 하드코딩|(password|token|apiKey|secret|api_key)\s*[:=]\s*[\x27"][^${\x27"][^\x27"]{3,}' 'SECRET_KNOWN|CRITICAL|알려진 비밀번호 패턴|(fukdon-riwbaq|hyung-ddfdf3|djg3-jj34|tkfactory-sub)' 'LOCALSTORAGE_AUTH|HIGH|localStorage 인증정보|localStorage\.(setItem|getItem).*\b(token|password|auth|credential)' 'INNERHTML_XSS|HIGH|innerHTML XSS 위험|\.innerHTML\s*[+=]' 'CORS_WILDCARD|HIGH|CORS 와일드카드|origin:\s*[\x27"`]\*[\x27"`]' 'SQL_INTERPOLATION|HIGH|SQL 문자열 보간|query\(`.*\$\{' 'LOG_SECRET|MEDIUM|로그에 비밀정보|console\.(log|error|warn).*\b(password|token|secret|apiKey)' 'ENV_HARDCODE|MEDIUM|환경설정 하드코딩|NODE_ENV\s*[:=]\s*[\x27"]development[\x27"]' ) EXCLUDE_PATTERNS="node_modules|\.git|__pycache__|package-lock\.json|\.min\.js|\.min\.css" # --- 메인 --- while read -r oldrev newrev refname; do # 브랜치 삭제 시 스킵 if [[ "$newrev" == "$ZERO_REV" ]]; then continue fi # 신규 브랜치 if [[ "$oldrev" == "$ZERO_REV" ]]; then # 첫 push: 최근 커밋만 검사 (또는 스킵) echo "[SECURITY] New branch detected — scanning latest commit only" oldrev=$(git rev-parse "${newrev}~1" 2>/dev/null || echo "$ZERO_REV") if [[ "$oldrev" == "$ZERO_REV" ]]; then continue fi fi # --- bypass 확인 --- BYPASS_FOUND=false BYPASS_REASON="" while IFS= read -r msg; do if echo "$msg" | grep -qP '\[SECURITY-BYPASS:\s*.+\]'; then BYPASS_FOUND=true BYPASS_REASON=$(echo "$msg" | grep -oP '\[SECURITY-BYPASS:\s*\K[^\]]+') elif echo "$msg" | grep -q '\[SECURITY-BYPASS\]'; then echo "[SECURITY] ERROR: Bypass requires reason: [SECURITY-BYPASS: hotfix 사유]" exit 1 fi done < <(git log --format='%s' "$oldrev".."$newrev" 2>/dev/null) if [[ "$BYPASS_FOUND" == "true" ]]; then AUTHOR=$(git log -1 --format='%ae' "$newrev" 2>/dev/null || echo "unknown") # 사용자 제한 ALLOWED=false for email in $ALLOWED_BYPASS_EMAILS; do if [[ "$AUTHOR" == "$email" ]]; then ALLOWED=true break fi done if [[ "$ALLOWED" != "true" ]]; then echo "[SECURITY] Bypass not allowed for: $AUTHOR" exit 1 fi # 로그 기록 echo "$(date -Iseconds) | user=$AUTHOR | ref=$refname | commits=$oldrev..$newrev | reason=$BYPASS_REASON | TODO=24h내 수정 필수" \ >> "$BYPASS_LOG" 2>/dev/null || true echo "[SECURITY] ⚠ Bypass accepted — reason: $BYPASS_REASON (logged, 24h 내 수정 필수)" continue fi # --- diff 기반 보안 검사 --- VIOLATIONS=0 MEDIUM_COUNT=0 OUTPUT="" DIFF_OUTPUT=$(git diff -U0 --diff-filter=ACMRT "$oldrev" "$newrev" 2>/dev/null || true) if [[ -z "$DIFF_OUTPUT" ]]; then continue fi CURRENT_FILE="" CURRENT_LINE=0 while IFS= read -r line; do if [[ "$line" =~ ^diff\ --git\ a/(.+)\ b/(.+)$ ]]; then CURRENT_FILE="${BASH_REMATCH[2]}" continue fi if [[ "$line" =~ ^\+\+\+\ b/(.+)$ ]]; then CURRENT_FILE="${BASH_REMATCH[1]}" continue fi if [[ "$line" =~ ^@@.*\+([0-9]+) ]]; then CURRENT_LINE="${BASH_REMATCH[1]}" continue fi if [[ "$line" =~ ^\+ ]] && [[ ! "$line" =~ ^\+\+\+ ]]; then local_content="${line:1}" # 제외 패턴 if echo "$CURRENT_FILE" | grep -qEi "$EXCLUDE_PATTERNS" 2>/dev/null; then CURRENT_LINE=$((CURRENT_LINE + 1)) continue fi # 인라인 ignore 체크 + 규칙 검사 for i in "${!RULES[@]}"; do IFS='|' read -r r_name r_sev r_desc r_pat <<< "${RULES[$i]}" if echo "$local_content" | grep -qP "$r_pat" 2>/dev/null; then # 라인 단위 ignore if echo "$local_content" | grep -qP "security-ignore:\s*$r_name" 2>/dev/null; then continue fi RNUM=$((i + 1)) TRIMMED=$(echo "$local_content" | sed 's/^[[:space:]]*//' | head -c 100) if [[ "$r_sev" == "CRITICAL" || "$r_sev" == "HIGH" ]]; then OUTPUT+="$(printf "\n ✗ [%s] #%d %s — %s\n → %s:%d\n %s\n" \ "$r_sev" "$RNUM" "$r_name" "$r_desc" "$CURRENT_FILE" "$CURRENT_LINE" "$TRIMMED")" VIOLATIONS=$((VIOLATIONS + 1)) else OUTPUT+="$(printf "\n ⚠ [%s] #%d %s — %s\n → %s:%d\n %s\n" \ "$r_sev" "$RNUM" "$r_name" "$r_desc" "$CURRENT_FILE" "$CURRENT_LINE" "$TRIMMED")" MEDIUM_COUNT=$((MEDIUM_COUNT + 1)) fi fi done CURRENT_LINE=$((CURRENT_LINE + 1)) fi done <<< "$DIFF_OUTPUT" TOTAL=$((VIOLATIONS + MEDIUM_COUNT)) if [[ $TOTAL -gt 0 ]]; then echo "" echo "[SECURITY] $TOTAL issue(s) found in push to $refname:" echo "$OUTPUT" echo "" if [[ $MEDIUM_COUNT -gt $MEDIUM_THRESHOLD ]]; then echo "[SECURITY] MEDIUM violations ($MEDIUM_COUNT) exceed threshold ($MEDIUM_THRESHOLD) — blocking" VIOLATIONS=$((VIOLATIONS + 1)) fi if [[ $VIOLATIONS -gt 0 ]]; then echo "Push rejected. Fix violations or use [SECURITY-BYPASS: 사유] in commit message." echo "" exit 1 else echo "Warnings only ($MEDIUM_COUNT MEDIUM) — push allowed." fi fi done exit 0