Skip to content

개선 리포트 실행: 0~4단계 보안 하드닝·기능 정합·품질 인프라 + 보류 3건 - #74

Merged
mack-erel merged 17 commits into
mainfrom
fix/security-hardening-phase1
Jul 5, 2026
Merged

개선 리포트 실행: 0~4단계 보안 하드닝·기능 정합·품질 인프라 + 보류 3건#74
mack-erel merged 17 commits into
mainfrom
fix/security-hardening-phase1

Conversation

@mack-erel

Copy link
Copy Markdown
Owner

개요

전체 코드베이스 분석 리포트(docs/improvement-report-2026-07-02.md)를 기반으로 0단계~4단계 + 보류 3건을 단계별로 처리했습니다. 각 단계는 타입체크·테스트·빌드·린트로 검증했고, 대형 작업(Refresh Token, SAML 암호화, i18n, CRUD 팩토리)은 병렬 에이전트 구현 후 작성과 분리된 독립 검증을 거쳤습니다.

검증(최종): vitest 54 passed(9 files) · svelte-check 0 errors(1349 files) · eslint·prettier 통과 · bun run build(adapter-cloudflare) 성공.

변경 요약 (17 커밋)

0단계 — 정리·정합성

  • users/[id] 14쿼리 병렬화, 스킨 sanitizer <style> 차단(CSS 인젝션), OIDC discovery 정합, 503 메시지 방언 중립화, wrangler 잡파일·평문 시크릿 삭제.

1단계 — 보안 하드닝

  • SAML LogoutRequest 파서 하드닝(DOCTYPE/ENTITY 차단·onErrorStopParsing·IssueInstant skew·Destination·replay-ID 소비), TOTP verify rate-limit+재사용 방지, rate-limit IPv6 /64 정규화(12개 호출부), admin oidc-clients CSRF 토큰(H-OIDC-5), find-id 타이밍 열거 차단, reset-password rate-limit, LDAP 레거시 평문 bindPassword 자동 암호화.
  • D1 바인딩을 Platform.env optional 타입으로 선언(adapter-node/방언 조합 대응).

2단계 — 기능 정합

  • Refresh Token(offline_access): 발급·회전·재사용 감지(RFC 6819), 로그아웃/비밀번호변경/권한변경 시 폐기.
  • OIDC Introspection(RFC 7662) / Revocation(RFC 7009) 엔드포인트.
  • SAML Assertion 암호화(EncryptedAssertion, AES-256-CBC + RSA-OAEP-mgf1p) + 라운드트립 검증 스크립트.
  • authorize 파라미터: prompt(none/login)·max_age·id_token_hint·login_hint.
  • admin users 목록 커서 페이지네이션 + 검색.

3단계 — 품질·운영

  • vitest 테스트 인프라 + 보안 로직 유닛 테스트 + 3방언 스키마 parity 테스트 + CI 테스트 단계.
  • 런타임 의존성 7종 dependencies 재분류, /api/health, 루트 +error.svelte, Workers observability.
  • 감사 로그 행 단위 무결성 HMAC(H-ADMIN-2).

4단계 + 보류 3건

  • 조직 CRUD 팩토리 + zod 폼 검증 + teams/parts FK 검증 신규, loopback 판정 공통화.
  • TOTP 이중 등록 방지: totp_owner_id nullable + 일반 unique index(4방언), enroll 원자화 + unique 위반 409.
  • i18n 영어 로케일: 엔진 ko 폴백 + SSR 로케일 결정 + en.json(엔드유저 플로우) + 언어 토글.

⚠️ 머지 전 확인 사항

  1. DB 마이그레이션은 생성만 했고 적용하지 않았습니다. (CLAUDE.md 규칙 준수)

    • 신규 마이그레이션: drizzle/0019_*(d1), drizzle/{pg,mysql,sqlite}/0002_*credentials.totp_owner_id 컬럼 + credentials_totp_owner_uidx 추가. 그리고 이전 단계의 audit_events.hash 마이그레이션(0018 / 0001).
    • 적용 방법(사용하는 방언에 맞게):
      bun run db:migrate           # D1 프로덕션
      bun run db:migrate:preview   # D1 프리뷰
      # PostgreSQL/MySQL: DATABASE_URL 설정 후 drizzle-kit migrate
      bun run db:migrate:pg
      bun run db:migrate:mysql
    • 원격 적용은 되돌리기 어려우므로 프리뷰에서 먼저 검증 권장.
  2. SAML Assertion 암호화 interop: 자체 라운드트립은 통과했으나 실제 SP(Shibboleth/ADFS/SimpleSAMLphp) XML-Enc 복호화기와의 상호운용은 별도 테스트 필요. 기본 off라 켜기 전 검증 권장.

  3. en.json 원어민 검수 권장: 엔드유저 인증 플로우만 영어, admin 콘솔은 ko 폴백으로 한국어 유지.

  4. 스킨 <style> 차단: 인라인 <style>에 의존하는 배포된 커스텀 스킨이 있으면 외관이 달라질 수 있음.

남은 항목(리포트에 기록)

argon2 WASM 전환 평가(현행 유지 권장), OIDC 스코프 확장(groups/address), 서버 에러 메시지 i18n, admin 콘솔 영어화 — 별도 작업으로 문서화.

🤖 Generated with Claude Code

mack-erel and others added 17 commits July 3, 2026 15:48
…시지 방언 중립화

- admin users/[id] load: 14개 순차 쿼리를 Promise.all 로 병렬화 (워터폴 제거)
- skin sanitizer: FORBIDDEN_TAGS 에 <style> 추가 — CSS 리드레싱/피싱 차단
- OIDC discovery: scopes_supported(+phone)·claims_supported 를 실제 발급 클레임과 일치
- guards.ts 503 메시지를 D1 전용 문구에서 방언 중립 문구로
- 전체 개선 리포트 문서 추가 (docs/improvement-report-2026-07-02.md)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- SAML LogoutRequest 파서에 DOCTYPE/ENTITY 차단 + onErrorStopParsing + IssueInstant skew(±5분),
  SLO 라우트에 Destination 대조 + 서명 검증 후 request-ID 1회용 소비, xmlEscape 제어문자 제거 (C1/C2/N-8)
- TOTP /verify·enroll/confirm 에 rate-limit + counter 기반 코드 재사용 방지 (C3)
- rate-limit IP 키를 IPv6 /64 로 정규화(normalizeIpForRateLimit), 12개 호출부 ipKey 전환,
  webauthn verify 2곳의 x-forwarded-for fallback 제거 (C6/N-12)
- admin oidc-clients 액션에 double-submit CSRF 토큰(auth/csrf.ts 신설) 적용 (H-OIDC-5)
- find-id 메일 발송을 응답 경로에서 분리해 타이밍 계정 열거 차단 (C5)
- reset-password 제출 action rate-limit 추가 (C8)
- LDAP 레거시 평문 bindPassword 를 로그인 시 자동 암호화 마이그레이션 (M-D)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
D1 을 선택적 방언으로 유지(D1 옵션 유지 결정)하기 위해, 모든 wrangler.jsonc 가
DB 바인딩을 선언하지는 않아도 코드가 타입상 성립하도록 app.d.ts 의
Platform.env 에 DB?: D1Database 를 추가. HYPERDRIVE/SKIN_CACHE 와 동일하게
optional 로 두어 방언·바인딩 조합 변경에도 타입체크가 그린으로 유지된다.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- oidc/refresh.ts 신설: issueRefreshToken/rotateRefreshToken(회전+재사용 감지)/
  revokeRefreshTokenFamily/revokeRefreshTokensForSession/revokeAllUserRefreshTokens.
  raw 토큰은 SHA-256 해시로만 저장, 회전은 revokedAt IS NULL 가드로 원자적 claim.
- token 엔드포인트: grant_type 분기 + 공통 토큰 빌더 추출.
  - authorization_code: offline_access scope + 클라이언트 refresh_token grant 허용 시 발급.
  - refresh_token: 회전 후 새 access/id/refresh token 발급, scope 축소 지원,
    재사용 감지 시 family 폐기(RFC 6819), 로그아웃된 세션 거부.
- 전역 무효화: 로그아웃(즉시/SAML SLO 체인)·비밀번호 재설정·관리자 role 변경 시 refresh token 폐기.
- discovery: grant_types_supported 에 refresh_token, scopes_supported 에 offline_access 광고.

svelte-check 0 errors(1192 files), eslint 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /oidc/introspect: access token(HMAC 검증) 및 refresh token(해시 조회) introspection.
  요청 클라이언트 소유 토큰만 active, 그 외 { active: false }.
- /oidc/revoke: refresh token 폐기(멱등). access token 은 stateless 라 no-op, 항상 200.
- oidc/client.ts: token/revoke/introspect 공통 authenticateOidcClient 헬퍼 추출.
- oidc/refresh.ts: findActiveRefreshToken / revokeRefreshTokenByValue 추가.
- discovery: introspection_endpoint / revocation_endpoint + auth methods 광고.
- 두 엔드포인트에 IP rate-limit(60/분).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- users load: createdAt 기준 커서 페이지네이션(PAGE_SIZE=50, +1 조회로 다음 페이지 판단).
- 방언 무관 lower() LIKE 검색(email/username/displayName), LIKE 와일드카드 이스케이프.
- UI: 검색창(GET) + 다음 페이지 링크(q 파라미터 보존), i18n 키(common.search/next_page, users.search_*).
- 대규모 테넌트에서 전 사용자 무제한 로드 문제(A3) 해소.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…C Core 3.1.2)

- prompt: none(무상호작용, 필요 시 login_required 를 redirect_uri 로), login(강제 재인증).
  none 과 타 값 동시 지정은 invalid_request.
- max_age: 세션 auth_time(createdAt) 초과 시 재인증(forceAuthn).
- id_token_hint: 서명 검증(만료 무시) 후 sub 가 현 세션 사용자와 다르면 재인증.
- login_hint: 로그인 페이지 아이디 입력란 프리필.
- verifyIdToken 에 ignoreExpiry 옵션 추가(id_token_hint 는 만료가 정상).

svelte-check 0 errors(1196 files), eslint 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- saml/encrypt.ts: WebCrypto 로 AES-256-CBC(assertion) + RSA-OAEP-mgf1p SHA-1(세션키)
  EncryptedAssertion 생성. 외부 XML-Enc 라이브러리 없이 구현.
- response.ts: 서명 이후·Response 서명 이전에 암호화. exc-c14n 서명이라 네임스페이스
  명시 선언해도 Assertion 서명 유효. importNode 로 EncryptedAssertion 치환.
- sso 라우트: sp.encryptAssertion + sp.cert 를 buildSignedSamlResponse 에 전달.
- admin saml-sps: create/update 에 encryptAssertion 처리(cert 없으면 활성 거부) + UI 토글.
- scripts/verify-saml-encryption.ts: 암호화→독립 복호화 라운드트립 검증(통과).
  package.json 에 verify:saml-encryption 스크립트 추가.

주의: 자체 라운드트립은 통과. 실제 SP XML-Enc 복호화기 상호운용은 별도 테스트 필요.
svelte-check 0 errors(1197 files), eslint 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- vitest.config.ts: SvelteKit 플러그인 없이 서버 라이브러리 유닛 테스트 (alias/env 스텁).
- test/unit: 26건 6파일 — IPv6 rate-limit 정규화, PKCE(S256/plain 거부), TOTP 재사용 방지,
  SAML 암호화 라운드트립, OIDC scope/redirect_uri 검증, 3방언 스키마 parity(E1).
- CI: Test 단계 추가 + test/**·vitest.config.* path filter. package.json test/test:watch 스크립트.
- E3: 런타임 서버 패키지 7종(@simplewebauthn/server, @peculiar/x509, @xmldom/xmldom,
  @yrneh_jang/ldapjs, reflect-metadata, xmldsigjs, xpath)을 dependencies 로 이동.

vitest 26 passed, svelte-check 0 errors, eslint/prettier 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /api/health: liveness + 얕은 DB readiness (hooks 가 baseline 조회 skip 하는 경로).
- src/routes/+error.svelte: 404/403/503 등 상태별 정돈된 에러 화면(기존 SvelteKit 기본 화면 대체).
- wrangler.example.jsonc: observability.enabled 추가 (Workers Logs).
- admin users 페이지네이션 링크를 resolve() 사용으로 수정(svelte/no-navigation-without-resolve).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- audit_events 에 hash 컬럼 추가(3방언). recordAuditEvent 가 안정 필드를 순서 고정
  직렬화해 IDP_SIGNING_KEY_SECRET 으로 HMAC-SHA256 계산·저장.
  DB write 권한만으로는 필드 변조/행 위조 불가. prev-hash 체인이 아닌 행 단위 MAC 이라
  동시 쓰기 fork 문제 없음(행 삭제 탐지는 Logpush 외부 미러 권장 — 주석 명시).
- computeAuditHash export + 유닛 테스트 3건(결정론성/변조탐지/키격리).
- find-password: SMTP 발송을 waitUntil 로 응답 경로에서 분리(find-id 와 동일, 타이밍 열거 차단).
- 마이그레이션 생성(적용 안 함): drizzle/0018, drizzle/{pg,mysql,sqlite}/0001.

vitest 29 passed, svelte-check 0 errors, eslint 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/lib/server/validation.ts 신설(isLoopbackHost). oidc-clients/saml-sps/skins 3곳에
  중복되던 loopback 판정을 공통 함수로 통합.

svelte-check 0 errors, vitest 29 passed, eslint 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- D2/E8/E5/find-password 완료 기록.
- E4 CRUD 팩토리+zod, D3 argon2, E10 i18n, TOTP TOCTOU 는 품질·리스크 사유로 보류(근거 명시).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- credentials 3방언 스키마에 nullable totp_owner_id + 일반 uniqueIndex(credentials_totp_owner_uidx) 추가.
  type='totp' 행만 userId 채우고 나머지는 NULL → NULL 은 unique 검사 제외(4방언 표준)라
  webauthn/backup_code 다중 보유 무영향, totp 만 사용자당 1개 DB 강제. mysql 은 TEXT unique 불가라 varchar(64).
  → partial unique index(mysql 미지원)를 회피해 signing_keys 류 drift 재생산 없음.
- enroll/confirm: TOTP+백업코드 INSERT 를 dialect 분기(d1/sqlite=batch, pg/mysql=transaction)로 원자화,
  totpOwnerId=userId 설정, unique 위반 try/catch → 409. 기존 rate-limit·counter 재사용방지 보존.
  백업코드는 TOTP INSERT 성공 시에만 커밋(고아 방지).
- 마이그레이션 생성만(d1 0019, pg/mysql/sqlite 0002) — 적용은 사용자(CLAUDE.md 준수).

독립 검증 통과. vitest 54 passed, svelte-check 0 errors, build 성공.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 엔진(i18n.svelte.ts): Locale="ko"|"en", t() 가 현재로케일→ko→원본key 순 폴백(누락 키는 한국어로,
  원본 key 노출 0). ko 회귀 없음.
- SSR 로케일: hooks.server.ts 가 쿠키(idp_locale)→Accept-Language(q-value)→기본 ko 로 결정,
  transformPageChunk 로 <html lang> 치환. +layout.server.ts 가 data.locale 전달, +layout.svelte 가
  렌더 전 setLocale → 하이드레이션 미스매치 없음. app.html lang 동적화, app.d.ts 에 Locals.locale.
- en.json: 엔드유저 인증 플로우(common/app/nav/login/mfa_login/signup/find_id/find_password/reset_password)
  영어 번역. admin/내부 섹션은 ko 폴백(부분영어 UX 회피). ※ 원어민 검수 권장.
- auth 3 화면(login/signup/reset) 잔존 하드코딩 한국어 → t(). LocaleToggle(쿠키+reload) 배치.
- 서버 +page.server.ts 에러 메시지는 범위 밖(미수정).

독립 검증 통과. app.d.ts 의 DB optional 보존(platform.env.DB 에러 재발 없음).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- zod 도입(dependencies). src/lib/server/admin/{crud-factory,schemas}.ts 신설.
- createAdminCrudRoute 팩토리: requireAdminContext → zod safeParse → tenant 스코프
  insert/update/delete(and(eq(id),eq(tenantId))) → recordAuditEvent(kind=`${prefix}_created|updated|deleted`)
  → 성공 반환. 에러 계약 보존(create=fail(400,{create:true,error}), update/delete=fail(400,{error})).
- teams/parts/positions/departments 4라우트를 팩토리로 통합. 훅 주입:
  departments 계층검증(self/순환/깊이8/참조무결성) 정확 이식, positions level zod 흡수,
  teams/parts 의 departmentId/teamId 동일 tenant FK 검증 신규 추가(기존 latent 결함 차단).
- zod 스키마: status z.enum, level/displayOrder coerce. UI(+page.svelte) 무변경.
- 유닛 테스트 2건(crud-factory, admin-zod).

독립 검증 통과. vitest 54 passed(9 files), svelte-check 0 errors, build 성공.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TOTP TOCTOU / i18n(en) / CRUD 팩토리+zod 의 게이트형 기획서·실행 투두. 완료·검증 상태 기록.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mack-erel
mack-erel merged commit a133a32 into main Jul 5, 2026
6 checks passed
@mack-erel
mack-erel deleted the fix/security-hardening-phase1 branch July 5, 2026 04:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant