현대 소프트웨어 개발에서 보안은 더 이상 “나중에 붙이는 옵션”이 아니다. 보안은 설계 이전에 존재하는 사고 방식(mental model)이다. 제품이 어떻게 동작해야 하는가가 아니라 어떻게 악용될 수 있는가를 먼저 생각하는 것이다. 결국 보안은 기술적 대응이 아니라, 가정과 위험을 지속적으로 검증하는 설계 중심 사고 체계다.
이 글은 특정 언어나 프레임워크의 보안 사용법이 아닌, 언어에 의존되지 않는 독립적인 보안 철학, 전략, 원칙들을 다룬다. 여러 프로그리밍 언어나 AI 도구를 오가며 작업할 때 보안에 대한 일관된 기준이 되고, 합당한 근거로 작용한다. 위임을 잘하려면 기준이 명확해야 한다. 이 기준과 더불어 AI 환경에 필요하거나 언어별 필요한 보안의 디테일한 특성들을 보완하면 더 환경에 적합한 보안 품질이 나온다.
1. 보안은 악용 시나리오에서 시작한다
보안에서 가장 중요한 사고방식의 전환은 이것이다.
“우리가 시스템을 어떻게 동작하게 만들 것인가”가 아니라, 공격자가 이 시스템을 어떻게 악용할 수 있는가 를 먼저 생각하는 것.
Microsoft SDL(Security Development Lifecycle)에서도 가장 강조하는 핵심 명제 중 하나가 바로 이것이다.
“설계 단계에서 보안을 고려하는 것은, 나중에 추가하는 것보다 비용이 훨씬 적고 위험도 훨씬 낮다.”
이 말은 단순한 비용 문제가 아니다. 설계 단계에서 신뢰 경계(Trust Boundary)나 인가 모델(Authorization Model)이 잘못 정의되면, 나중에 코드 패치나 보안 기능을 덧붙이는 것으로는 근본적인 문제를 해결하기 매우 어렵다. 구조 자체가 취약하게 설계된 경우가 많기 때문이다.
OWASP Top 10에서 A06: Insecure Design(2025 기준, 이전 버전에서는 A04로 분류)이 독립된 항목으로 올라온 이유도 바로 여기에 있다. 이 취약점은 코드 레벨의 버그가 아니라, 설계 자체의 결함 에서 비롯되기 때문에 단순한 코드 수정으로는 고치기 어렵다. 설계 재검토와 구조 변경이 필요하다.
Threat Modeling의 본질
SDL Practice 3에서 정의한 Threat Modeling의 핵심은 시각의 전환이다.
- 기존: “이 기능은 어떻게 정상적으로 동작하는가?”
- 보안 관점: “이 기능은 어떻게 악용될 수 있는가?”
기능 명세(Functional Specification)와 함께 악용 명세(Abuse Specification) 를 함께 작성하는 습관을 들이는 것이, 진정한 보안 설계의 출발점이다.
Business Logic Vulnerabilities의 위험
특히 주의해야 할 부분이 비즈니스 로직 취약점(Business Logic Vulnerabilities) 이다. 이 취약점의 무서운 점은, 기능 자체는 완벽하게 정상적으로 동작한다는 것이다. 하지만 공격자가 여러 기능을 예상치 못한 순서나 조합으로 사용하면 치명적인 문제가 발생한다.
실제 사례로 자주 등장하는 것들:
- 할인 코드의 중복 적용이나 무제한 사용
- 결제 흐름 우회 (예: 결제 완료 전에 주문 상태 변경)
- 권한 검증이 누락된 다단계 처리
- 상태 전환을 비정상적인 순서로 조작
이런 공격들은 SAST나 DAST 같은 자동화 도구로는 거의 탐지되지 않는다. 코드의 결함이 아니라, 설계 단계에서 악용 시나리오를 고려하지 않은 결과 이기 때문이다.
2. Threat Modeling: 설계에 공격 관점을 통합하라
Threat Modeling은 단순히 보안 문서를 작성하는 작업이 아니다. 설계 단계부터 공격자의 관점을 의식적으로 통합하는 지속적인 사고 과정이다. 가장 중요한 점은, 이것이 보안 팀만의 전유물이 아니라는 것이다. 설계자, 개발자, 아키텍트, 운영자까지 함께 참여하는 협업 활동이어야 진정한 효과를 발휘한다. Microsoft SDL에서는 Threat Modeling을 다음과 같은 다섯 단계로 진행한다.
Threat Modeling의 5단계
-
자산 파악 (Asset Identification)
시스템이 다루는 모든 중요한 자산을 명확히 식별한다.
개인정보, 인증 토큰, 결제 정보, 내부 API 키, 관리자 권한 등 “보호해야 할 것”을 구체적으로 나열한다. 코드에서 실제로 다루는 민감 데이터를 중심으로 파악하는 것이 실무적이다. -
아키텍처 개요 작성
Data Flow Diagram(DFD)을 그리고, 신뢰 경계(Trust Boundary) 를 명확히 정의한다.
어디까지가 신뢰할 수 있는 영역이고, 어디서부터가 신뢰할 수 없는 영역인지 구분하는 작업이다. 이 경계가 모호하면 대부분의 보안 취약점이 발생한다. -
위협 식별
여기서 STRIDE 방법론을 주로 사용한다. 시스템이 받을 수 있는 위협을 체계적으로 분류하는 강력한 도구다. -
완화 방안 개발
식별된 위협에 대해 계층화된 방어(Defense in Depth)를 설계한다. 하나의 방어책에만 의존하지 않고, 여러 겹의 방어를 고려한다. -
추적과 커뮤니케이션
발견된 위협과 대응 방안을 문서화하고, 팀 전체와 공유하며 실제 설계와 구현에 반영한다.
STRIDE 위협 모델
| 범주 | 설명 | 주요 대응 방안 |
| Spoofing | 신원 위장 | 강력한 인증, 토큰 검증, 세션 관리 |
| Tampering | 데이터 위조/변조 | 입력 검증, 무결성 검증, 디지털 서명 |
| Repudiation | 행위 부인 | 감사 로그, 불변 로그, 디지털 서명 |
| Information Disclosure | 민감 정보 노출 | 데이터 암호화, 최소 응답 원칙 |
| Denial of Service | 서비스 거부 | Rate Limiting, 타임아웃, 리소스 보호 |
| Elevation of Privilege | 권한 상승 | 최소 권한 원칙, 객체 수준 인가 검증 |
실무에서 중요한 점
Threat Modeling을 한 번 하고 끝내는 것은 큰 의미가 없다. 기능이 추가되거나 기존 기능이 크게 변경될 때마다 해당 부분의 Data Flow와 Trust Boundary를 다시 검토해야 한다. 처음에는 부담스럽게 느껴질 수 있지만, 익숙해지면 설계 회의 자체가 자연스럽게 “이걸 공격자가 어떻게 비틀 수 있을까?”라는 질문을 포함하게 된다. 이것이 바로 성숙한 보안 문화다.
3. 신뢰 경계를 코드로 표현하라
Threat Modeling에서 그린 Trust Boundary(신뢰 경계)는 다이어그램에만 그려놓고 끝나면 아무 의미가 없다. 진짜 중요한 것은 그 경계를 실제 코드에서 명시적으로 표현하고, 강제하는 것 이다. 신뢰 경계를 넘어오는 모든 데이터는 반드시 검증되어야 한다. “내부 서비스끼리 주 고받는 거니까 안전할 거야”라는 가정은 매우 위험하다. 내부라고 해서 예외를 두는 순간, 그 지점이 공격 표면이 된다.
실제 코드에서 Trust Boundary가 나타나는 주요 지점
-
외부 입력 경계
- HTTP 요청 파라미터, 쿼리스트링, 헤더, 바디
- 파일 업로드
- 환경 변수, CLI 인자
-
서비스 간 경계
- 마이크로서비스 간 API 호출
- 메시지 큐(Kafka, RabbitMQ 등)로 주고받는 메시지
- 외부 서드파티 서비스 응답
-
권한 경계
- 일반 사용자 → 관리자 권한으로의 전환
- 권한 상승이 필요한 민감 기능 접근 시점
왜 내부 요청도 검증해야 하는가
많은 개발자들이 “내부에서 호출하는 거니까 안전하겠지”라고 생각하지만, 이는 큰 착각이다. 실제 공격 사례를 보면 내부 시스템 간 호출을 이용한 공격이 상당히 많다.
대표적인 예가 바로 SSRF(Server-Side Request Forgery) 다. OWASP API Security Top 10에서도 API7: Server Side Request Forgery 로 명시된 취약점이다.
// 잘못된 예: 외부 입력 URL을 그대로 사용
async function fetchResource(url: string) {
return fetch(url) // SSRF 취약점
}
// 올바른 예: 허용 목록 기반 검증
const ALLOWED_HOSTS = ['api.internal', 'data.internal']
async function fetchResource(url: string) {
let hostname: string
try {
hostname = new URL(url).hostname
} catch {
throw new Error('유효하지 않은 URL')
}
if (!ALLOWED_HOSTS.includes(hostname)) {
throw new Error('허용되지 않은 호스트')
}
return fetch(url)
}핵심 원칙
- 모든 Trust Boundary를 넘는 데이터는 명시적인 검증을 거쳐야 한다.
- “내부에서 온 요청”이라는 사실 자체를 신뢰해서는 안 된다.
- 허용 목록(Allowlist) 기반 검증을 우선적으로 고려한다. 블랙리스트는 쉽게 우회될 수 있다.
- 검증 로직은 가능한 한 경계 지점에서 일찍 수행한다.
신뢰 경계를 코드로 명확하게 표현한다는 것은, “이 지점부터는 더 이상 신뢰하지 않는다”는 선언을 코드로 하는 행위다. 이 선언이 없으면 설계 단계에서 아무리 Threat Modeling을 잘 해도 실제 보호는 이루어지지 않는다.
4. 검증되지 않은 가정이 취약점이 된다
단락 3에서 “어디서 검증할 것인가(경계의 위치)“를 다뤘다면, 이 절은 “무엇을 의심해야 하는가(가정의 종류)“를 점검한다. 보안 실패의 근본 원인은 코드 자체보다, 검증되지 않은 가정(Assumption)에 있다. “신뢰하되 검증하라(Trust, but verify)“는 충분하지 않다. 보안에서 정확한 원칙은 “검증 없이는 신뢰하지 않는다(Never trust, always verify)“다. 신뢰를 전제하느냐 배제하느냐의 차이가 설계 전체를 바꾼다.
실제로 많은 취약점은 세 가지 잘못된 가정에서 발생한다.
- 입력 신뢰: 사용자 입력이 의도대로만 들어올 것이라는 가정
- 권한 과신: 인증된 사용자는 모든 리소스에 접근 가능하다는 가정
- 환경 신뢰: 내부 서비스 또는 외부 라이브러리는 안전하다는 가정
이 가정들은 코드로 명시적으로 깨뜨릴 수 있다. 사용자가 입력한 내용은 절대 “코드”나 “명령어”로 실행하면 안 되고, “데이터”로만 취급해야 한다. eval, exec, spawn은 되도록 쓰지 않도록 한다.
// 위험한 코드 예시
const userInput = "process.exit(0)"; // 또는 더 위험한 명령어
eval(userInput); // ← 절대 하면 안 되는 코드타입 시스템으로 잘못된 입력을 컴파일 시점에 차단하고, 스키마 검증으로 런타임 경계를 명시하며, 객체 레벨 권한 검증으로 “인증 = 모든 데이터 접근 가능”이라는 가정을 제거한다.
// 잘못된 예: 인증만 확인, 소유권은 가정
async function getDocument(userId: string, docId: string) {
return db.documents.findById(docId) // 인증된 사용자라면 타인의 문서도 조회 가능
}
// 올바른 예: 객체 레벨 권한 검증 (OWASP BOLA 대응)
async function getDocument(userId: string, docId: string) {
const doc = await db.documents.findById(docId)
if (!doc || doc.ownerId !== userId) {
throw new ForbiddenError('접근 권한 없음')
}
return doc
}세 가지 가정은 서로 다른 공격 벡터로 이어진다. “환경 신뢰” 가정의 가장 흔한 사례는 시크릿 하드코딩이다. API 키나 DB 비밀번호를 코드에 직접 넣으면 git 이력에 영구 보존되고, 저장소가 공개되는 순간 즉시 노출된다.
// 잘못된 예: 환경을 신뢰, 시크릿을 코드에 직접 기재
const apiKey = 'sk-proj-xxxxxxxxxxxx'
const db = new Database({ password: 'admin1234' })
// 올바른 예: 환경 변수로 분리, 미설정 시 명시적으로 실패
const apiKey = process.env.OPENAI_API_KEY
if (!apiKey) throw new Error('OPENAI_API_KEY가 설정되지 않았습니다')
const db = new Database({ password: process.env.DB_PASSWORD })또 다른 “환경 신뢰” 사례로는 AI 에이전트의 간접 프롬프트 인젝션이 있다(GitHub Security Lab 연구). 코드 저장소·이슈·PR 같은 개발 환경 컨텍스트에 공격자가 악성 지시를 삽입하면, AI 에이전트는 이를 신뢰된 명령으로 처리해 인증 토큰 탈취나 악성 커밋으로 이어질 수 있다. “AI 에이전트가 읽는 컨텍스트는 안전하다”는 가정 자체가 공격 표면이 된다. 공격자는 코드를 깨는 것이 아니라, 개발자의 가정을 깨는 존재다.
KISA 소프트웨어 개발보안 가이드가 입력 데이터 검증, 보안 기능, 에러 처리, 코드 품질을 독립된 항목으로 구분해 다루는 것도 같은 맥락이다. 각 항목은 “여기서 무엇을 신뢰하고 있는가”를 점검하는 체크리스트로 읽힌다.
5. 알려진 패턴을 반복 적용하라
보안 취약점의 대부분은 새로운 문제가 아니라 반복되는 패턴이다. OWASP는 이를 체계화한 가장 대표적인 기준이다.
OWASP Top 10 2025(2025년 초 공개 예정 draft 기준) 주요 항목을 보면 패턴이 보인다. 2021 버전과 항목 순서 및 번호가 다르니 주의가 필요하다.
| 순위 | 항목 | 설계 실패 원인 | 사례 |
| A01 | 접근 제어 우회 | 권한 검증 누락 | 다른 사용자/테넌트 ID 직접 접근(IDOR), 관리자 API 직접 호출, 객체 수준 권한 체크 누락 |
| A02 | 보안 설정 오류 | 기본값 신뢰 | 디버그 모드 활성화 상태 배포, S3/스토리지 공개 설정, CORS 와일드카드 허용 |
| A03 | 소프트웨어 공급망 실패 | 외부 의존성 신뢰 | 악성 npm 패키지 포함, 업데이트된 라이브러리 변조, CI/CD 빌드 파이프라인 오염 |
| A04 | 암호화 결함 | 민감 데이터 처리 미흡 | 평문 비밀번호 저장, 약한 해시(MD5/SHA1), TLS 검증 비활성화 |
| A05 | 인젝션 | 입력 검증 부재 | SQL 주입( 시간 기반), NoSQL 주입($where), 프롬프트 인젝션(LLM 명령 실행) |
| A06 | 안전하지 않은 설계 | 설계 단계 보안 누락 | rate limit 없음, 권한 모델 미정의, 비즈니스 로직 우회 가능 구조 |
| A07 | 인증 실패 | 인증 메커니즘 약점 | JWT 알고리즘 혼란(RS256→HS256), 토큰 만료 미검증, OAuth state 미검증 |
| A08 | 소프트웨어·데이터 무결성 실패 | 업데이트·데이터 검증 부재 | 서명 검증 없는 업데이트, 웹훅 위조 요청 처리, 캐시 데이터 변조 |
| A09 | 보안 로깅·경보 실패 | 탐지·감시 미비 | 로그인 실패 로그 미수집, 침입 알림 없음, 이상 행위 탐지 부재 |
| A10 | 예외 조건 오류 처리 | 오류 처리 취약점 | 상세 스택트레이스 노출, fail-open 처리, 예외 시 인증 우회 |
각 항목의 설계 실패 원인을 보면 공통 패턴이 드러난다. 입력 검증 부재, 권한 검증 누락, 기본값 신뢰가 A01·A02·A05·A07을 포함한 다수 항목의 근본 원인이다. 이 세 가지를 코드 리뷰 체크리스트에 명시적으로 포함하는 것이 실질적인 출발점이 된다. A03(공급망)·A04(암호화)처럼 별도의 아키텍처 결정이 필요한 항목도 있지만, 세 가지 원인이 교차되는 취약점 영역이 가장 넓다.
2025 버전에서 A03 소프트웨어 공급망 실패가 독립 항목으로 분리된 점은 주목할 만하다. 2021 버전까지는 A08(소프트웨어·데이터 무결성 실패) 안에 포함됐지만, npm 등 패키지 레지스트리를 통한 공급망 공격의 빈도와 영향 범위가 커지면서 별도 항목이 됐다. 외부 의존성은 이제 코드베이스의 일부가 아니라 독립적인 공격 표면이다. SBOM(Software Bill of Materials)은 이 공격 표면을 가시화하는 수단이다. 소프트웨어에 포함된 모든 구성 요소와 버전을 기록한 명세서로, 새로운 CVE가 공개됐을 때 영향받는 컴포넌트를 즉시 특정할 수 있고, 침해 발생 후 범위를 신속하게 파악하는 데도 활용된다.
패턴을 알았다면, 그것을 반복 적용하는 과정을 자동화해야 한다. 코드 자체의 취약점은 SAST로, 런타임 취약점은 DAST로, 의존성 취약점은 SCA로, 시크릿 노출은 전용 스캐너로 탐지한다.
| 분류 | 도구 | 특징 | 적합한 상황 |
| SAST | Semgrep | 정적 분석, 500+ 보안 규칙 내장, 커스텀 규칙 작성 가능 | 코드 작성·PR 단계 |
| DAST | OWASP ZAP | 실행 중인 애플리케이션을 대상으로 런타임 취약점 탐지 | 스테이징·QA 단계 |
| DAST | Nuclei | 대규모 엔드포인트 스캔, 최신 CVE 및 Known Vulnerability 탐지, Misconfiguration 발견, 인증이 필요한 API 테스트 | 스테이징·QA 단계 |
| SCA | Dependabot | GitHub 기본 제공, 취약 의존성 자동 PR 생성 | GitHub 프로젝트 |
| SCA | OSV-Scanner | Google 개발, OSV.dev DB 활용, 11개 이상 언어·19개 이상 락파일 지원 | 로컬·CI 통합, 다양한 언어 생태계 |
| SCA + 기타 | Trivy | 컨테이너 이미지·파일시스템·Git 저장소 스캔, 시크릿·설정 오류도 탐지 | 컨테이너 기반 서비스 |
| SBOM | syft | 컨테이너·파일시스템에서 SBOM 자동 생성, CycloneDX·SPDX 포맷 지원 | 공급망 보안, 의존성 가시화 |
| 시크릿 탐지 | Gitleaks | Git 이력 전체에서 시크릿 패턴 탐지, pre-commit 훅 통합 가능 | 시크릿 하드코딩 방지 |
SCA 도구 선택 기준: GitHub 프로젝트는 Dependabot을 기본으로 활성화한다. 다중 언어 생태계이거나 GitHub 외 환경이라면 OSV-Scanner를 병행 또는 대체한다. 컨테이너 기반 서비스는 Trivy 단독으로 SCA·시크릿·설정 오류를 함께 커버할 수 있다.
도구는 실행 시점이 중요하다. 배포 후에 발견하면 git 이력 재작성, 시크릿 로테이션, 영향 범위 파악까지 수습 비용이 커진다. SAST → SCA → 시크릿 탐지를 PR 머지 전 CI 게이트에 순서대로 두는 것이 Shift Left의 핵심이다. 문제를 오른쪽(배포·운영)에서 발견할수록 비용은 기하급수적으로 증가한다.
다만 도구는 알려진 CVE와 시크릿 패턴을 탐지하는 것이지, 설계 결함이나 인가 오류를 찾아주지 않는다. 패턴 지식 없이 도구만 실행하면 알림이 쏟아지는데 어디서부터 대응해야 할지 판단하기 어렵다. 도구는 OWASP 패턴 지식의 보완재이지 대체재가 아니다.
보안은 창의적 대응이 아니라, 검증된 취약점 패턴을 이해하고 반복적으로 적용하는 구조적 학습의 문제다. 그러나 패턴과 도구가 모든 공격을 막아주지는 않는다. 남은 질문은 “공격이 성공했을 때 어떻게 살아남을 것인가”다.
6. 침해를 전제로 설계하라
보안은 모든 공격을 완전히 차단하는 것이 아니라, 공격의 성공을 지연시키고 피해 범위를 제한하는 전략이다. 이 전략은 시간축에 따라 두 가지 원칙으로 나뉜다. 침해 발생 전의 방어 설계(Defense in Depth)와 침해 발생 후의 생존 설계(Assume Breach)다.
Defense in Depth는 여러 계층의 통제를 쌓는 접근이다. 단일 방어가 뚫리더라도 다음 계층이 공격을 탐지하거나 지연시킨다. 코드 레벨에서는 스키마 검증, 인가 검증, 에러 처리, 감사 로그 등이 각각 독립적인 방어 계층으로 작동한다. 하나가 실패하더라도 다른 계층이 보완할 수 있도록 설계하는 것이 중요하다.
Assume Breach는 침해가 이미 발생했다고 전제하고 시스템을 설계하는 원칙이다. 공격이 성공했을 때 피해 범위(Blast Radius)를 최소화하는 방법을 사전에 결정해야 한다.
구체적으로 최소 권한 원칙(Least Privilege)을 적용해 하나의 계정이 탈취당하더라도 전체 시스템이 노출되지 않도록 하고, 서비스 간 격리를 통해 공격자의 이동 경로를 차단하며, 충분한 감사 로그를 남겨 침해 시점과 범위를 빠르게 파악한다. OWASP A09(보안 로깅·경보 실패)가 강조되는 이유도 바로 여기에 있다. 공격이 진행 중일 때 탐지하지 못하면 대응할 시간을 잃어버리기 때문이다.
// Information Disclosure 방어 — 내부 오류 정보를 응답에 포함하지 않는다
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
// 스택 트레이스는 로그에만, 응답에는 절대 포함하지 않음
logger.error({ err, requestId: req.id })
res.status(500).json({ error: '요청을 처리할 수 없습니다' })
})공급망 보안 역시 같은 논리로 접근해야 한다. 외부 의존성이 이미 침해됐다고 가정하고, 의존성 서명 검증과 최소 권한 토큰을 적용해 피해 범위를 사전에 제한한다. 안전한 시스템이란 “뚫리지 않는 시스템”이 아니라, 뚫리더라도 쉽게 무너지지 않는 시스템이다.
마치며
여섯 가지 원칙은 각각 독립적이지만 하나의 방향을 가리킨다. 보안을 기능 외부에서 관리하는 것이 아니라, 코드를 작성하는 모든 결정 속에 내재화하는 것이다.
- 악용 시나리오를 기능 명세와 함께 작성한다
- Threat Modeling으로 공격 관점을 설계에 통합한다
- 신뢰 경계를 코드로 명시적으로 표현한다
- 검증되지 않은 가정을 코드에서 드러내고 제거한다 (시크릿 하드코딩 포함)
- 알려진 취약점 패턴을 설계와 코드 리뷰에 반복 적용한다
- 침해를 전제로 피해 범위를 설계 단계에서 제한한다
보안은 담당 팀의 영역이 아니라 설계자, 개발자, 운영자 모두가 공유하는 태도다. 기능을 구현하는 것만큼, 그 기능이 어떻게 무너질 수 있는지를 함께 고민하는 것이 보안의 본질이다.




