3년 전, 제가 컨설팅하던 스타트업이 72시간 만에 모든 것을 잃는 모습을 봤습니다. 정교한 국가 공격이나 헤드라인을 장식한 제로데이 악용 때문이 아니었습니다. 그들은 주니어 개발자가 급하게 배포한 금요일에 도입한 단 하나의 SQL 인젝션 취약점 때문에 고객 데이터베이스 전체와 평판, 결국 사업까지 잃었습니다. 공격은 월요일 아침에 발생했습니다. 수요일 오후에는 그들이 파산 서류를 작성하고 있었습니다.
💡 주요 내용
- 모두를 잡는 인증 함정
- 교차 사이트 스크립팅: 결코 사라지지 않는 취약점
- SQL 인젝션: 여전히 유효한 고대의 취약점
- HTTPS는 더 이상 선택 사항이 아니다
저는 사라 첸이며, 지난 12년 동안 다양한 스타트업에서 포춘 500 대기업에 이르기까지 보안 아키텍트로 일해왔습니다. 제가 보기에 방지 가능한 동일한 실수들이 반복해서 비즈니스를 파괴하는 모습을 지켜보았습니다. 가장 답답한 부분은? 이러한 재난의 대부분은 평균적인 JavaScript 프레임워크를 배우는 것보다 짧은 시간에 배울 수 있는 기본 보안 지식만 있으면 피할 수 있었던 일들입니다.
누구도 코드 학습을 시작할 때 말해주지 않는 것이 있습니다: 보안은 다른 모든 것을 마스터한 후에 다루는 고급 주제가 아닙니다. 이것은 기본적입니다. 빨간 불이 멈추라는 뜻임을 모른 채 운전하는 것과 같습니다. 잠시 동안은 괜찮을 수 있지만 결국에는 재앙적인 사고를 일으킬 것입니다.
2023년 Verizon 데이터 유출 조사 보고서에 따르면, 74%의 유출 사건에는 인간 요소가 포함되어 있으며, 오류, 오용 또는 사회공학이 포함되어 있습니다. 그러나 여기서 주목할 점은: 그들이 웹 애플리케이션 공격을 분석했을 때, 86%가 10년 이상 문서화되고 예방 가능한 취약점을 악용했다는 것입니다. 우리는 정교한 공격자에게 지고 있는 것이 아니라 기본에 대한 자신의 무지로 지고 있는 것입니다.
모두를 잡는 인증 함정
인증에 대해 말씀드리겠습니다. 여기서 저는 개발자들이 처음으로 치명적인 실수를 저지르는 모습을 봅니다. 그들은 그것을 전체 보안 모델의 기초라기보다 체크박스 기능처럼 취급합니다. 저는 한 번 이메일이 쿠키에 존재하는지 확인하여 인증을 구현한 코드를 검토한 적이 있습니다. 서명된 쿠키도 아니고, 암호화된 토큰도 아닙니다. 누구나 브라우저의 개발 도구에서 수정할 수 있는 일반 텍스트 이메일 주소였습니다.
제가 이를 지적했을 때, 개발자는 "하지만 잘 작동해!"라고 말했습니다. 바로 그 점이 문제입니다. 보안 취약점은 오류 메시지로 자신을 알리지 않습니다. 누군가 악용할 때까지 완벽하게 작동합니다.
적절한 인증에는 실제로 다음이 필요합니다. 먼저 인증(내가 누구인지 증명하는 것)과 권한 부여(내가 할 수 있는 일을 증명하는 것)의 차이를 이해해야 합니다. 이 개념이 혼란스러웠던 운영 시스템을 봤을 때, 하나의 보안 문제를 수정하는 것이 세 개의 문제를 발생시킬 수 있었습니다.
비밀번호 저장은 협의의 여지가 없습니다. bcrypt, scrypt 또는 Argon2와 같은 적절한 비밀번호 해시 알고리즘을 사용해야 합니다. SHA-256도 아니고, MD5도 아니며, 절대적으로 일반 텍스트도 아닙니다. 2026년에도 여전히 비밀번호가 일반 텍스트로 저장된 데이터베이스를 만나며, 매번 개발자는 "아무도 해킹할 것이라고는 생각하지 않았다"고 말합니다. 이는 누군가 절대적으로 도둑질할 것이라고 생각하지 않아서 현관문을 잠그지 않는 것과 같습니다.
여기 숫자는 엄청납니다. 2023년 신원 도용 리소스 센터 보고서에 따르면, 미국에서만 3,205건의 데이터 유출이 발생했으며, 3억 5천3백만 명 이상의 피해자에게 영향을 미쳤습니다. 연구자들이 유출된 데이터를 분석했을 때, 비밀번호 저장 방법이 공개된 경우 43%가 적절하지 않은 해시 또는 아예 해시를 사용하지 않았다고 합니다.
세션 관리가 흥미로워지는 곳입니다. 암호학적으로 안전한 무작위 세션 토큰을 생성해야 합니다. 대부분의 언어에서 내장된 난수 생성기는 암호학적으로 안전하지 않습니다. Node.js에서는 crypto.randomBytes()를 사용해야 하며, Math.random()은 사용하지 않아야합니다. Python에서는 secrets.token_hex()를 원하고 random.random()은 사용하지 않아야 합니다. 타임스탬프와 사용자 ID를 연결하여 생성된 세션 토큰을 본 적이 있습니다. 공격자는 몇 초 안에 이를 예측할 수 있습니다.
당신의 세션 토큰은 최소 128비트의 엔트로피를 가져야 합니다. 이들은 오직 HTTPS를 통해 전송되어야 하며, JavaScript가 접근할 수 없도록 HttpOnly 플래그가 설정되어야 합니다. 결코 암호화되지 않은 연결을 통해 전송되지 않도록 Secure 플래그가 설정되어야 하고, CSRF 공격을 방지하기 위해 SameSite 속성이 설정되어야 합니다. 그리고 만료되어야 합니다. 민감한 애플리케이션에 대해서는 비활동 30분 후에 만료되도록 하고, 낮은 위험의 경우 24시간일 수 있습니다.
교차 사이트 스크립팅: 결코 사라지지 않는 취약점
XSS 공격은 바퀴벌레와 같습니다. 오래전부터 존재했고 모두가 알고 있지만 여전히 프로덕션 코드에서 나타납니다. OWASP Top 10은 2003년 초창기부터 XSS 취약점을 포함했고, 2026년인 지금에도 여전히 존재합니다. 이것은 같은 예방 가능한 취약점이 21년 간 존재해온 것입니다.
"보안은 다른 모든 것을 마스터한 후에 다루는 고급 주제가 아닙니다. 이것은 기본적입니다. 빨간 불이 멈추라는 뜻임을 모른 채 운전하는 것과 같습니다."
의사가 입력한 환자 노트를 표시하는 의료 애플리케이션을 감사했던 기억이 납니다. 개발자들은 기본적인 형식을 허용하는 리치 텍스트 편집기를 구축했습니다. 합리적으로 들리죠? 그러나 그들은 HTML 입력을 아무런 정화 없이 페이지에 직접 렌더링하고 있었습니다. 저는 스크립트 태그를 포함한 환자 노트를 입력하여 취약점을 증명했습니다. 그 스크립트는 세션 토큰을 훔치거나, 의료 기록을 수정하거나, 환자 데이터를 유출할 수 있었습니다. 개발자들은 충격을 받았습니다. 의사가 악의적일 수도 있고, 의사의 컴퓨터가 손상될 경우 악성 코드를 주입할 수도 있다는 점을 고려한 적이 없었습니다.
근본적인 원칙은 다음과 같습니다: 사용자 입력을 절대 신뢰하지 마세요. 의사의 입력이든, 관리자든, CEO든 상관 없습니다. 애플리케이션 외부에서 오는 모든 데이터는 증명될 때까지 잠재적으로 악의적입니다.
당신이 이해해야 할 XSS의 세 가지 유형이 있습니다. 저장된 XSS는 악의적인 코드가 데이터베이스에 저장되고, 그 데이터가 조회될 때마다 실행되는 경우입니다. 반사된 XSS는 URL 매개변수에 있는 악의적인 코드가 즉시 응답에 반사되는 경우입니다. DOM 기반 XSS는 클라이언트 측 JavaScript가 사용자 입력을 받아 페이지에 적절한 정화 없이 삽입하는 경우입니다.
해결책은 복잡하지 않지만, 규율이 요구됩니다. 상황에 따라 출력을 이스케이프해야 합니다. HTML 콘텐츠에 데이터를 삽입할 경우 HTML 엔티티 인코딩이 필요합니다. JavaScript 문자열에 데이터를 삽입할 경우 JavaScript 이스케이프가 필요합니다. URL에 데이터를 삽입할 경우 URL 인코딩이 필요합니다. CSS에 데이터를 삽입할 경우 CSS 이스케이프가 필요합니다.
현대 프레임워크들은 이를 돕습니다. React, Vue, Angular는 기본적으로 출력을 이스케이프합니다. 그러나 이것은 마법이 아닙니다. React에서 dangerouslySetInnerHTML 또는 Vue에서 v-html을 사용하면 이러한 보호 기능을 우회하는 것입니다. "HTML을 렌더링할 필요가 있었다"는 이유로 이러한 기능을 사용하는 개발자를 본 적이 있으며, 자신이 XSS 취약점을 열어놓고 있다는 사실을 이해하지 못했습니다.
콘텐츠 보안 정책은 두 번째 방어선입니다. CSP는 브라우저에게 어떤 콘텐츠 출처가 합법적인지 알려주는 HTTP 헤더입니다. 인라인 스크립트를 완전히 차단하고, 어떤 도메인이 JavaScript를 로드할 수 있는지를 제한하며, 전체 XSS 공격 클래스를 예방할 수 있습니다. 구글의 연구에 따르면, 엄격한 CSP를 구현하면 XSS 공격의 약 95%를 예방할 수 있습니다. 그러나 제 경험상 제가 감사한 애플리케이션의 30% 미만이 CSP를 가지고 있으며, 그 중 대부분은 너무 관대해서 사실상 쓸모가 없습니다.
SQL 인젝션: 여전히 유효한 고대의 취약점
SQL 인젝션은 멸종되어야 합니다. 이것은 1990년대부터 잘 이해되어 왔습니다. 보비 테이블은 10년 넘게 밈으로 알려져 있습니다. 그럼에도 불구하고, Akamai의 2023년 인터넷 상태 보고서에 따르면 SQL 인젝션 시도로 인해 모든 웹 애플리케이션 공격의 65%를 차지합니다. 왜일까요? 여전히 작동하기 때문입니다.
| 취약점 유형 | 위험 수준 | 방지 난이도 | 일반적인 원인 |
|---|---|---|---|
| SQL 인젝션 | 치명적 | 쉬움 | 쿼리에서 비정화된 사용자 입력 |
| 약한 인증 | 치명적 | 쉬움 | 부적절한 비밀번호 정책, MFA 없음 |
| XSS (교차 사이트 스크립팅) | 높음 | 중간 | HTML의 비이스케이프된 사용자 콘텐츠 |
| 접근 제어 오류 | 높음 | 중간 | 부족한 권한 검증 |
| 보안 구성 오류 | 중간 | 쉬움 | 기본 설정, 노출된 엔드포인트 |
한 번은 8년 동안 운영된 전자상거래 회사에 대해 컨설팅을 했습니다. 그들은 수백만 건의 거래를 처리했으며, 15명의 개발자 팀이 있었습니다. 그리고 그들의 검색 기능은 SQL 인젝션에 취약했습니다. 코드 구조는 대략 이러했습니다: URL 매개변수에서 검색어를 받아 SQL 쿼리에 직접 연결했습니다. 매개변수화도 없었고, 입력 검증도 없었습니다. 아무것도 없었습니다.
제가 그 취약점을 데모할 때...