보안 감사 결과 CRITICAL 2건, HIGH 5건 발견 → 수정 완료 + 자동화 구축. [보안 수정] - issue-view.js: 하드코딩 비밀번호 → crypto.getRandomValues() 랜덤 생성 - pushSubscriptionController.js: ntfy 비밀번호 → process.env.NTFY_SUB_PASSWORD - DEPLOY-GUIDE.md/PROGRESS.md/migration SQL: 평문 비밀번호 → placeholder - docker-compose.yml/.env.example: NTFY_SUB_PASSWORD 환경변수 추가 [보안 강제 시스템 - 신규] - scripts/security-scan.sh: 8개 규칙 (CRITICAL 2, HIGH 4, MEDIUM 2) 3모드(staged/all/diff), severity, .securityignore, MEDIUM 임계값 - .githooks/pre-commit: 로컬 빠른 피드백 - .githooks/pre-receive-server.sh: Gitea 서버 최종 차단 bypass 거버넌스([SECURITY-BYPASS: 사유] + 사용자 제한 + 로그) - SECURITY-CHECKLIST.md: 10개 카테고리 자동/수동 구분 - docs/SECURITY-GUIDE.md: 운영자 가이드 (워크플로우, bypass, FAQ) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
6.1 KiB
Bash
Executable File
169 lines
6.1 KiB
Bash
Executable File
#!/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
|