--- name: security-and-hardening description: 취약점에 대비해 코드를 하드닝합니다. 입력 핸들러의 취약점을 감사할 때, 사용자 입력, 인증, 데이터 저장, 외부 연동을 다룰 때, 또는 로그인 흐름이 OWASP Top Ten에 안전한지 확인할 때 사용하세요. 신뢰할 수 없는 데이터를 받는 기능, 사용자 세션이나 세션 쿠키를 관리하는 기능, 제3자 서비스와 상호작용하는 기능을 만들 때 사용하세요. 알려진 취약점에 대해 의존성을 감사하거나, 패키지 매니저 감사 결과를 트리아지하거나, 새 패키지의 공급망 위험을 평가할 때 사용하세요. 개인 데이터나 프라이버시 준수(GDPR, CCPA)가 관련될 때 사용하세요. --- # Security and Hardening ## 개요 웹 애플리케이션을 위한 보안 우선 개발 관행입니다. 외부에서 들어오는 입력은 모두 적대적인 것으로 보고, 시크릿은 모두 절대 새면 안 되는 것으로 다루며, 인가 검사는 모두 빠뜨리지 마세요. 보안은 별도 단계가 아닙니다. 사용자 데이터, 인증, 외부 시스템을 건드리는 모든 코드 줄에 걸리는 제약 조건입니다. ## 언제 사용하나 - 사용자 입력을 받는 것을 만들 때 - 인증이나 인가를 구현할 때 - 민감한 데이터를 저장하거나 전송할 때 - 외부 API나 서비스와 연동할 때 - 파일 업로드, 웹훅, 콜백을 추가할 때 - 결제 데이터나 PII를 다룰 때 ## 절차: 위협 모델부터 위협 모델 없이 나중에 붙인 통제는 추측입니다. 하드닝하기 전에 5분만 공격자의 입장에서 생각하세요. 1. **신뢰 경계를 그리세요.** 신뢰할 수 없는 데이터가 시스템 안으로 들어오는 지점은 어디입니까? HTTP 요청, 폼 필드, 파일 업로드, 웹훅, 제3자 API, 메시지 큐, **LLM 출력**입니다. OS가 건네줘서 내부처럼 보이는 로컬 값도 포함됩니다. 다른 프로세스의 명령줄이나 환경, 공유 볼륨의 파일 이름, 작업 페이로드 안의 경로입니다. 신뢰는 값을 전달한 채널이 아니라, 값을 *쓴* 주체를 따릅니다. 모든 경계가 공격 표면입니다. 2. **자산을 이름 붙이세요.** 훔치거나 깨뜨릴 가치가 있는 것은 무엇입니까? 자격 증명, PII, 결제 데이터, 관리자 동작, 금전 이동입니다. 3. **각 경계에 STRIDE를 적용하세요.** 의식처럼 거창하게 하지 말고, 빠른 렌즈로 보세요. | 위협 | 질문 | 일반적인 완화 | |---|---|---| | **S**poofing (위장) | 누군가 사용자나 서비스를 사칭할 수 있습니까? | 인증, 서명 검증 | | **T**ampering (변조) | 전송 중이나 저장 중에 데이터가 바뀔 수 있습니까? | 무결성 검사, 매개변수화 쿼리, HTTPS | | **R**epudiation (부인) | 나중에 어떤 동작을 부인할 수 있습니까? | 보안 이벤트 감사 로깅 | | **I**nformation disclosure (정보 노출) | 데이터가 새어 나갈 수 있습니까? | 암호화, 필드 허용 목록, 일반적인 오류 메시지 | | **D**enial of service (서비스 거부) | 감당하지 못할 만큼 요청이 몰릴 수 있습니까? | 요청 제한, 입력 크기 상한, 타임아웃 | | **E**levation of privilege (권한 상승) | 사용자가 없어야 할 권한을 얻을 수 있습니까? | 인가 검사, 최소 권한 | 4. **사용 사례 옆에 오용 사례를 적으세요.** 기능마다 "이것을 어떻게 악용할까?"를 묻고, 그 질문을 첫 테스트로 만드세요. 기능의 신뢰 경계를 이름 붙일 수 없다면, 그 기능을 보안할 준비가 되지 않은 것입니다. 이것이 OWASP **A04: Insecure Design**입니다. 대부분의 침해는 코드가 아니라 설계에서 시작됩니다. ## 3단계 경계 체계 ### 항상 할 일 (예외 없음) - **시스템 경계에서 모든 외부 입력을 검증하세요** (API 라우트, 폼 핸들러) - **모든 데이터베이스 쿼리를 매개변수화하세요.** 사용자 입력을 SQL에 이어 붙이지 마세요 - **XSS를 막도록 출력을 인코딩하세요** (프레임워크 자동 이스케이프를 쓰고, 우회하지 마세요) - **모든 외부 통신에 HTTPS를 사용하세요** - **비밀번호는 bcrypt/scrypt/argon2로 해시하세요** (평문은 절대 저장하지 마세요) - **보안 헤더를 설정하세요** (CSP, HSTS, X-Frame-Options, X-Content-Type-Options) - **세션에는 httpOnly, secure, sameSite 쿠키를 사용하세요** - **릴리스 전에, 커밋된 락파일을 대상으로 감지된 패키지 매니저의 네이티브 감사를 실행하세요** ### 먼저 물어볼 일 (사람 승인 필요) - 새 인증 흐름을 추가하거나 인증 로직을 변경 - 민감 데이터의 새 범주(PII, 결제 정보)를 저장 - 새 외부 서비스 연동을 추가 - CORS 설정을 변경 - 파일 업로드 핸들러를 추가 - 요청 제한이나 스로틀링을 수정 - 높은 권한이나 역할을 부여 ### 절대 하지 말 것 - **시크릿을 버전 관리에 커밋하지 마세요** (API 키, 비밀번호, 토큰) - **민감한 데이터를 로그에 남기지 마세요** (비밀번호, 토큰, 신용카드 번호 전체) - **클라이언트 측 검증을 보안 경계로 신뢰하지 마세요** - **편의를 위해 보안 헤더를 끄지 마세요** - **사용자가 제공한 데이터에 `eval()`이나 `innerHTML`을 쓰지 마세요** - **세션을 클라이언트가 접근할 수 있는 저장소에 두지 마세요** (인증 토큰용 localStorage) - **스택 트레이스나 내부 오류 세부 정보를 사용자에게 노출하지 마세요** ## 하드닝 통제 아래 규칙이 워크플로우입니다. 각각의 구체적인 구현은 [references/hardening-patterns.md](references/hardening-patterns.md)에 있습니다. 그 코드에 도달했을 때 필요한 절을 여세요. 그 전에 열지 마세요. ### 인젝션, XSS, 접근 제어 - 모든 쿼리를 매개변수화하세요. 입력 문자열로 SQL, NoSQL, 셸 명령을 만들지 마세요. - 프레임워크의 자동 이스케이프로 출력을 인코딩하세요. 원시 HTML이 불가피하면, 먼저 허용 목록 새니타이저로 정화하세요. - 모든 요청에서 인증만이 아니라 **인가**를 검사하세요. 인증된 사용자가 그 리소스를 소유하거나, 그 리소스에 대한 허가를 받아야 합니다 (A01, IDOR). 패턴: [인젝션](references/hardening-patterns.md#injection), [XSS](references/hardening-patterns.md#cross-site-scripting-xss), [접근 제어](references/hardening-patterns.md#broken-access-control). ### 인증과 세션 - 비밀번호는 bcrypt(12라운드 이상), scrypt, argon2로 해시하세요. 세션 시크릿은 코드가 아니라 환경에서 옵니다. - 세션 쿠키는 `httpOnly`, `secure`, `sameSite: 'lax'` 또는 `'strict'`입니다. 이것이 CSRF 방어입니다. `'none'`은 크로스 사이트 요청에도 쿠키를 보냅니다. `maxAge`에는 상한을 두세요. 패턴: [인증](references/hardening-patterns.md#broken-authentication). ### 헤더, CORS, 응답 - 모든 응답에 보안 헤더를 두세요 (helmet 또는 프레임워크에 해당하는 것). CSP는 `default-src 'self'`에서 시작해 범위를 조이세요. 풀어 두지 마세요. - CORS는 설정에 있는 명시적 오리진 목록으로 제한하세요. 자격 증명과 함께 `*`를 쓰지 마세요. - 어떤 응답을 보내기 전에 민감 필드(`passwordHash`, 재설정 토큰)를 빼세요. 오류 본문은 일반적인 내용만 담고, 내부 정보는 서버 로그에만 남기세요. 패턴: [설정 오류](references/hardening-patterns.md#security-misconfiguration), [민감 데이터 노출](references/hardening-patterns.md#sensitive-data-exposure). ### 입력 검증과 업로드 - 경계에서 스키마로 검증하세요. 허용된 형태, 길이, enum, 형식입니다. 422와 구조화된 세부 정보로 거부하세요. 그 아래 코드는 파싱되어 타입이 정해진 값만 사용합니다. - 업로드: MIME 타입을 허용 목록으로 두고, 크기를 제한하고, 중요할 때는 내용(매직 바이트)을 검증하세요. 확장자는 아무것도 증명하지 않습니다. 패턴: [스키마 검증](references/hardening-patterns.md#schema-validation-at-boundaries), [파일 업로드](references/hardening-patterns.md#file-upload-safety). ### 서버 측 요청 (SSRF) 사용자가 영향을 주는 URL은 무엇이든, 웹훅, URL에서 가져오기, 이미지 프록시, 링크 미리보기처럼, 내부 서비스를 겨냥할 수 있습니다. 스킴과 호스트를 허용 목록으로 제한하고, **모든** DNS 레코드를 확인한 뒤 사설이거나 예약된 주소(루프백, 링크 로컬 `169.254.169.254`, 사설, 유니크 로컬. IPv4와 IPv6 모두)는 거부하고, 리다이렉트를 금지하세요. 이 검사에도 DNS 리바인딩으로 인한 TOCTOU 간격이 남습니다. 위험이 높은 표면에서는 확인된 IP를 고정하거나, 앞에 필터링 에이전트를 두세요. 패턴: [SSRF](references/hardening-patterns.md#server-side-request-forgery-ssrf). ### 파생 경로에 대한 파괴적 작업 삭제, 이동, 덮어쓰기는 대상을 가리키는 값만큼만 안전합니다. 신뢰는 그 값을 전달한 채널이 아니라, 그 값을 *쓴* 주체를 따릅니다. 다른 프로세스의 명령줄은 폼 필드만큼 공격자가 통제할 수 있습니다. 형태 검사는 형식이 올바른지만 증명합니다. 인가를 증명하지는 않습니다. 호출 전에 세 가지를 모두 요구하세요. 확인된 대상(심볼릭 링크를 해석한 뒤)이 **허용된 루트** 아래에 있을 것, 그 루트보다 최소 한 단계 **아래**일 것, **작업 전에 읽은 소유 증거**를 가질 것. 거부하면 거부된 대상을 로그하고 멈추세요. 더 넓은 기본 경로로 후퇴하지 마세요. 검사가 읽히는 것보다 약한 이유(마커 자기 증명, 검사/사용 경합): [파괴적 경로](references/hardening-patterns.md#destructive-operations-on-derived-paths). 동작하는 코드: `../../references/security-checklist.md`. ### 요청 제한 API는 일반적으로 제한하고, 인증 엔드포인트는 엄격하게 제한하세요(약 15분에 10회). 트래픽을 처리하는 프로세스가 둘 이상이 되면, 메모리 안 카운터는 조용히 `max × instances`가 되거나 서버리스에서는 아예 발동하지 않습니다. 리미터를 공유 저장소로 뒷받침하세요. 패턴: [요청 제한](references/hardening-patterns.md#rate-limiting). ### 시크릿 시크릿은 환경에서 옵니다. `.env.example`은 플레이스홀더와 함께 커밋하고, 실제 `.env*` 파일과 키 자료는 gitignore하세요. 커밋하기 전에 스테이징된 diff를 grep하세요. **시크릿이 리모트에 닿으면, 도착하는 순간 유출된 것입니다. 먼저 교체한 다음 이력을 지우세요.** 패턴: [시크릿 관리](references/hardening-patterns.md#secrets-management). ### 의존성과 공급망 1. **설치 경계와 매니저를 찾으세요.** 락파일을 소유한 워크스페이스 루트를 사용하세요. 독립된 중첩 프로젝트는 그 워크스페이스 밖에 있을 때만 사용하세요. `packageManager`(있을 때), 락파일, CI를 서로 대조하세요. 어긋나거나 락파일이 경합하면 멈추세요. 매니저 버전을 고정하세요. 2. **첫 실행 전에 의존성 스크립트를 막으세요.** 스크립트를 끈 채로, 또는 문서화된 fail-closed 정책으로 부트스트랩하세요. 대기 중인 스크립트 소스를 검사하고, 최소한만 승인한 뒤 정책을 커밋하고, 깨끗한 frozen/immutable 설치로 검증하세요. 일괄 승인하지 마세요. 3. **매 릴리스 전에 커밋된 락파일을 대상으로 네이티브 감사를 실행하세요.** critical/high는 **도달 가능성**(런타임, 빌드, 테스트, 배포 경로)과 수정본이 있는지로 트리아지하세요. 강제 조치(`npm audit fix --force` 또는 그에 해당하는 명령)를 자동으로 적용하지 마세요. 강제 수정은 선언된 의존성 범위를 벗어날 수 있습니다. 미리 보고, 변경 이력을 읽고, 업그레이드마다 테스트하세요. 미루는 항목마다 이유와 재검토 날짜를 문서화하세요. 4. **감사는 알려진 권고와만 일치합니다.** 새로 악의적이거나 타이포스쿼팅된 패키지(`cross-env`와 `crossenv`)는 잡지 못합니다. 새 의존성, 락파일 diff, 스크립트 정책 변경을 함께 리뷰하세요. 소유권, 유지보수 상태, 릴리스 후 경과 시간, 프로비넌스, 전이 그래프입니다. 지원되는 곳에서는 레지스트리 서명을 검증하세요(`npm audit signatures`, `pnpm audit signatures`). 서명이 없으면 침해의 자동 증거로 보지 말고, 조사할 신호로 보세요 (A06, LLM03). 트리아지 결정 트리: [의존성 감사 트리아지](references/hardening-patterns.md#dependency-audit-triage). 매니저 매트릭스와 설치 스크립트 게이트: `../../references/security-checklist.md`. ### 개인 데이터와 프라이버시 하드닝은 "공격자가 이것을 읽을 수 있는가?"를 묻습니다. 프라이버시는 "우리가 이것을 아예 보유해야 하는가, 그리고 얼마나 오래 보유하는가?"를 묻습니다. 보호하고, 침해를 막고, 규정을 지키는 비용이 가장 낮은 데이터는 처음부터 수집하지 않은 데이터입니다. 개인 데이터를 최소화해야 할 부채로 대하세요. - **필드를 추가할 때 분류하세요**(비개인, PII, 민감). 등급마다 그에 맞게 다루세요. 찾을 수 없는 데이터는 보호할 수도 없고, 삭제 요청을 이행할 수도 없습니다. - **명시한 목적에 대해서만 수집하세요.** "나중에 쓸모가 있을 수도 있다"는 목적이 아니라, 잠재된 침해 범위입니다. 텔레메트리에 PII를 넣지 마세요(`observability-and-instrumentation` 스킬이 운영 쪽에서 같은 점을 말합니다). - **보존 기간을 미리 정한 다음, 실제로 삭제하세요.** 개인 데이터 저장소마다 TTL과 동작하는 삭제 경로가 필요합니다. 백업, 캐시, 검색 인덱스, 분석 사본을 포함합니다. - **관할 지역이 요구하는 정보 주체 권리를 지원하세요**(GDPR, CCPA 및 같은 계열의 규정). 내보내기, 정정, 삭제입니다. 사용자 데이터를 찾을 수 있고 지울 수 있도록 스키마를 설계하세요. 여러 시스템에 되돌릴 수 없이 흩뿌려 두지 마세요. - **동의는 수집과 제3자 공유의 관문이며, 감사할 수 있어야 합니다.** 분석, 광고, LLM 벤더에 PII를 보내는 것은 공유입니다. 벤더에게는 데이터 처리 계약이 필요합니다. 지역을 하드코딩된 가정이 아니라 설정 가능한 정책으로 두세요. 분류 표: [데이터 분류](references/hardening-patterns.md#data-classification). 프라이버시 사고가 나면 침해 통지 기한이 시작됩니다. 사후 분석은 `debugging-and-error-recovery` 스킬로 진행하세요. ### AI / LLM 기능 LLM을 호출하면(챗봇, 요약기, 에이전트, RAG) 새로운 공격 표면이 생깁니다. [LLM 애플리케이션을 위한 OWASP Top 10 (2025)](https://genai.owasp.org/llm-top-10/)에 대응시키세요. - **모델 출력은 신뢰할 수 없는 입력입니다**(LLM05). `eval`, SQL, 셸, `innerHTML`, 파일 경로에 넣지 마세요. 방어적으로 파싱하고, 스키마로 검증한 다음, 인코딩하세요. - **프롬프트는 탈취될 수 있습니다**(LLM01). 컨텍스트 안의 신뢰할 수 없는 텍스트(사용자 메시지, 가져온 페이지, PDF)가 지시를 실어 올 수 있습니다. 시스템 프롬프트는 보안 경계가 아닙니다. 권한은 코드에서 강제하세요. - **시크릿, 다른 테넌트의 데이터, 시스템 프롬프트 전문을 컨텍스트 창 밖에 두세요**(LLM02, LLM07). 도구 권한의 범위를 한정하고, 도구 인자를 모두 검증하고, 파괴적 동작은 확인하세요(LLM06). 토큰, 요청 속도, 재귀 깊이에 상한을 두세요(LLM10). RAG 임베딩은 테넌트별로 나누고, 색인하기 전에 문서를 검증하세요(LLM08). 패턴: [LLM 출력 처리](references/hardening-patterns.md#llm-output-handling). ## 리뷰 체크리스트 승인 전에 `../../references/security-checklist.md`를 하나씩 따라가세요. 인증, 인가, 입력, 데이터 보호와 프라이버시, 헤더와 CORS, 의존성과 공급망, AI/LLM, 오류 처리, 그리고 OWASP 빠른 참조 표를 다룹니다. ## 흔한 합리화 | 합리화 | 현실 | |---|---| | "내부 도구니까 보안은 중요하지 않다" | 내부 도구도 침해됩니다. 공격자는 가장 약한 고리를 노립니다. | | "보안은 나중에 넣으면 된다" | 보안을 나중에 끼워 넣는 일은 처음부터 만드는 것보다 10배 어렵습니다. 지금 넣으세요. | | "아무도 이걸 악용하려 하지 않을 것이다" | 자동 스캐너가 찾아냅니다. 숨겨 두는 것은 보안이 아닙니다. | | "프레임워크가 보안을 처리해 준다" | 프레임워크는 도구를 줄 뿐, 보장하지 않습니다. 올바르게 쓰는 일은 여전히 여러분 몫입니다. | | "프로토타입일 뿐이다" | 프로토타입은 프로덕션이 됩니다. 첫째 날부터 보안 습관을 들이세요. | | "여기서 위협 모델링은 과하다" | "이걸 어떻게 공격할까?"를 5분만 생각해 보면, 나중에 어떤 통제도 메울 수 없는 설계 결함을 막을 수 있습니다. | | "LLM 출력이고 텍스트일 뿐이다" | 그 "텍스트"는 SQL 문, 스크립트 태그, 셸 명령일 수 있습니다. 다른 신뢰할 수 없는 입력과 같이 대하세요. | | "감사를 통과했으니 의존성은 안전하다" | 감사는 알려진 권고와 맞출 뿐입니다. 새로 악의적인 패키지를 찾아내지 못하고, 리뷰하지 않은 설치 스크립트를 실행해도 되는 것으로 만들지 않습니다. | | "지금 모아 두자. 나중에 필요할 수도 있다" | 보유하지 않은 데이터는 침해될 수도, 제출 명령을 받을 수도, 잘못 삭제될 수도 없습니다. "필요할 수도 있다"는 목적이 아니라 침해 범위입니다. | | "삭제 요청은 수동으로 처리하면 된다" | 수동 삭제는 백업, 캐시, 분석 사본을 놓칩니다. 스키마가 사용자 데이터를 찾지 못하면 요청을 이행할 수 없습니다. 처음부터 그렇게 설계하세요. | | "규정 준수는 법무 문제이지, 우리 문제가 아니다" | 내보내기, 삭제, 보존, 동의는 스키마와 코드의 일입니다. PII를 열 개 시스템에 흩뿌린 뒤에 법무가 덧붙일 수는 없습니다. | ## 레드 플래그 - 사용자 입력이 데이터베이스 쿼리, 셸 명령, HTML 렌더링으로 그대로 넘어갑니다 - 삭제, 이동, 덮어쓰기의 대상이 페이로드, 설정 값, 다른 프로세스의 명령줄에서 오는데, 경로의 형태 검사만으로 지켜집니다 - 소스 코드나 커밋 이력에 시크릿이 있습니다 - 인증이나 인가 검사가 없는 API 엔드포인트가 있습니다 - CORS 설정이 없거나 와일드카드(`*`) 오리진을 씁니다 - 인증 엔드포인트에 요청 제한이 없거나, 인스턴스가 둘 이상인데 메모리 안 리미터를 씁니다 - 스택 트레이스나 내부 오류가 사용자에게 노출됩니다 - 알려진 critical 취약점이 있는 의존성, 하나의 설치 경계에서 경합하는 락파일, 재현할 수 없는 설치, 일괄 승인된 스크립트가 있습니다 - 서버가 허용 목록 없이 사용자가 준 URL을 가져옵니다 (SSRF) - LLM/모델 출력이 쿼리, DOM, 셸, `eval`로 넘어갑니다 - 시크릿, PII, 시스템 프롬프트 전문이 LLM 컨텍스트 창 안에 들어갑니다 - 명시한 목적, 보존 한도, 삭제 경로 없이 개인 데이터를 수집합니다 - 동의나 데이터 처리 계약 없이 PII를 분석/광고/LLM 벤더에 보냅니다 - "계정 삭제"가 삭제 플래그만 뒤집고, 개인 데이터는 저장소와 백업에 남습니다 ## 검증 보안과 관련된 코드를 구현한 뒤: - [ ] 네이티브 감사에, 완화되지 않은 채 도달 가능한 critical/high 발견이 없습니다. CI는 기준이 되는 락파일을 보존하고, 리뷰되지 않은 의존성 스크립트를 막습니다 - [ ] 소스 코드와 git 이력에 시크릿이 없습니다 - [ ] 모든 사용자 입력을 시스템 경계에서 검증합니다 - [ ] 파괴적인 파일시스템 작업은 심볼릭 링크를 해석한 뒤, 실행 전에 허용된 루트, 최소 깊이, 소유를 확인합니다 - [ ] 보호된 엔드포인트마다 인증과 인가를 검사합니다 - [ ] 응답에 보안 헤더가 있습니다 (브라우저 DevTools로 확인) - [ ] 오류 응답이 내부 세부 정보를 노출하지 않습니다 - [ ] 인증 엔드포인트에서 요청 제한이 동작하고, 트래픽을 처리하는 인스턴스가 둘 이상이면 공유 저장소로 뒷받침됩니다 - [ ] 서버 측 URL 가져오기를 허용 목록으로 검증합니다 (SSRF 없음) - [ ] LLM/모델 출력을 쓰기 전에 검증하고 인코딩합니다 (AI 기능이 있는 경우) - [ ] 개인 데이터를 분류하고, 명시한 목적으로 최소화하며, 보존 한도를 둡니다 - [ ] 삭제와 내보내기 요청이 끝까지 동작합니다 (백업, 캐시, 분석 사본 포함)