보안은 기능이 아니라 품질이다
보안은 로그인 화면을 만들거나 HTTPS를 켜는 일로 끝나지 않는다. 개발자가 작성하는 입력 처리, 권한 확인, 로그 출력, 데이터 저장 방식 전체에 스며 있는 품질이다. 성능 문제가 늦은 응답으로 드러난다면, 보안 문제는 데이터 유출, 권한 없는 조작, 서비스 중단, 신뢰 상실로 드러난다.
보안의 기본 목표는 크게 세 가지로 볼 수 있다. 기밀성은 보지 말아야 할 사람이 데이터를 보지 못하게 하는 것이다. 무결성은 데이터가 허가 없이 바뀌지 않았음을 보장하는 것이다. 가용성은 필요한 사용자가 서비스를 계속 사용할 수 있게 하는 것이다. 여기에 사용자가 누구인지 확인하는 인증, 무엇을 할 수 있는지 결정하는 인가, 행동을 추적하는 감사 로그가 붙는다.
개발자가 보안을 이해해야 하는 이유는 단순하다. 대부분의 취약점은 코드의 작은 가정에서 시작된다. "이 값은 프론트엔드에서 막았으니 안전하다", "로그인했으니 이 주문도 볼 수 있겠지", "내부 API니까 검증하지 않아도 된다" 같은 가정이 공격자의 입구가 된다. 보안은 모든 입력을 의심하고, 모든 권한을 명시적으로 확인하며, 실패를 안전한 방향으로 처리하는 습관에서 시작한다.
AI 생성 이미지
인증과 인가는 다르다
인증(Authentication)은 "당신이 누구인가"를 확인하는 과정이다. 비밀번호, 일회용 코드, 패스키, 소셜 로그인, 세션 쿠키, JWT 검증이 여기에 속한다. 사용자가 로그인에 성공하면 서버는 이 요청이 어떤 사용자에게 속하는지 알 수 있다.
인가(Authorization)는 "그 사용자가 이 작업을 해도 되는가"를 판단하는 과정이다. 이미 로그인한 사용자라도 모든 데이터에 접근할 수 있는 것은 아니다. 예를 들어 주문 상세 API가 다음과 같다고 하자.
GET /orders/1004서버가 단순히 "로그인했는가"만 확인하고 주문 ID 1004를 그대로 조회해 반환하면 문제가 생긴다. 사용자가 URL의 숫자를 바꿔 다른 사람의 주문을 볼 수 있기 때문이다. 올바른 처리는 주문을 조회한 뒤 order.user_id == current_user.id인지 확인하거나, 처음부터 WHERE id = ? AND user_id = ?처럼 현재 사용자 범위 안에서만 조회하는 것이다.
관리자 기능도 마찬가지다. 프론트엔드에서 관리자 메뉴를 숨기는 것은 사용자 경험을 위한 처리일 뿐, 보안 경계가 아니다. 공격자는 브라우저 개발자 도구나 직접 만든 HTTP 요청으로 숨겨진 API를 호출할 수 있다. 권한 확인은 반드시 서버에서 수행해야 한다.
실무에서 자주 생기는 보안 사고는 인증 실패보다 인가 실패에서 나온다. 로그인 시스템은 잘 만들었지만, 객체 단위 권한 확인을 빠뜨려 다른 사용자의 문서, 결제 정보, 파일에 접근하게 되는 식이다. 따라서 민감한 API에는 "이 요청의 사용자는 누구인가"와 "이 사용자가 이 구체적인 자원에 접근할 수 있는가"라는 질문이 모두 필요하다.
해시, 암호화, 전자서명, TLS는 목적이 다르다
보안 기술은 이름이 비슷해도 달성하는 목표가 다르다. 이 차이를 모르면 비밀번호를 암호화해서 저장하거나, 서명된 토큰을 암호화된 토큰으로 착각하는 실수를 한다.
해시(Hash)는 입력을 고정 길이 값으로 바꾸는 단방향 함수다. 중요한 특징은 원래 값을 되돌릴 수 없다는 점이다. 파일이 변조되었는지 확인하거나 비밀번호 검증에 사용된다. 단, 일반 해시 함수인 SHA-256을 비밀번호에 한 번 적용하는 것만으로는 충분하지 않다. 비밀번호는 사람이 기억하기 쉬운 값이라 공격자가 대량으로 추측할 수 있기 때문이다. 비밀번호 저장에는 Argon2id, bcrypt, PBKDF2처럼 일부러 계산 비용을 크게 만든 적응형 해시 알고리즘을 사용하고, 사용자마다 고유한 salt를 붙여야 한다1.
암호화(Encryption)는 데이터를 읽을 수 없는 형태로 바꾸고, 올바른 키를 가진 쪽이 다시 복호화할 수 있게 하는 기술이다. 즉 양방향이다. 데이터베이스에 저장된 주민등록번호, API 토큰, 백업 파일처럼 나중에 원문이 필요한 데이터에는 암호화를 사용할 수 있다. 하지만 비밀번호는 보통 원문을 다시 알 필요가 없다. 로그인 시 입력한 비밀번호를 같은 방식으로 해시해 저장된 해시와 비교하면 된다. 그래서 비밀번호는 암호화가 아니라 해시로 저장하는 것이 기본이다.
암호화의 안전성은 알고리즘 이름만으로 결정되지 않는다. 키를 어떻게 만들고, 어디에 저장하고, 누가 접근할 수 있으며, 언제 회전할 수 있는지가 더 중요할 때가 많다. OWASP는 검증된 알고리즘과 안전한 모드를 사용하고, 키를 데이터와 분리하며, KMS나 HSM 같은 안전한 키 관리 시스템을 활용할 것을 권장한다2. 직접 만든 암호 알고리즘을 쓰거나, 키를 소스 코드와 같은 저장소에 커밋하는 것은 피해야 한다.
전자서명(Digital Signature)은 데이터가 특정 주체에 의해 만들어졌고 중간에 변조되지 않았음을 검증하는 기술이다. 개인키로 서명하고 공개키로 검증한다. 예를 들어 JWT가 서명되어 있으면 서버는 토큰의 내용이 발급 후 바뀌지 않았는지 확인할 수 있다. 하지만 서명은 암호화가 아니다. 일반적인 JWT의 payload는 Base64URL로 인코딩되어 있을 뿐이라 누구나 읽을 수 있다. 따라서 서명된 토큰 안에 비밀번호, 주민등록번호, 장기 비밀키 같은 민감 정보를 넣으면 안 된다.
TLS는 네트워크로 이동 중인 데이터를 보호한다. HTTPS는 HTTP 위에 TLS를 적용한 형태다. TLS는 클라이언트가 진짜 서버와 통신하는지 확인하고, 전송 중인 데이터를 제3자가 읽거나 바꾸기 어렵게 만든다. 다만 TLS는 데이터가 서버에 도착한 뒤의 저장 방식이나 서버 내부 권한 문제를 해결하지 않는다. OWASP는 TLS 1.3을 기본으로 사용하고, TLS 1.0/1.1과 오래된 SSL 버전을 비활성화하며, 인증서와 개인키를 안전하게 관리할 것을 권장한다3.
대표적인 웹 취약점은 입력과 신뢰 경계에서 생긴다
웹 보안 취약점의 상당수는 외부 입력을 코드, 쿼리, HTML, 권한 판단에 그대로 섞을 때 발생한다. 사용자가 보낸 값은 URL, 헤더, 쿠키, JSON 바디, 업로드 파일, 메시지 큐 어디에서 오든 신뢰할 수 없는 입력으로 다뤄야 한다.
SQL 인젝션은 사용자 입력이 SQL 문장의 일부로 해석될 때 발생한다. 예를 들어 로그인 쿼리를 문자열 연결로 만들면 공격자가 이메일 입력칸에 SQL 조각을 넣어 조건문을 바꿀 수 있다.
SELECT * FROM users
WHERE email = '${email}' AND password_hash = '${hash}'방어의 핵심은 사용자 입력을 SQL 코드로 만들지 않는 것이다. prepared statement, parameterized query, ORM의 바인딩 기능을 사용하면 입력값은 SQL 문법이 아니라 값으로 전달된다. 입력 검증도 필요하지만, 검증만으로 SQL 인젝션을 막겠다는 접근은 위험하다. 쿼리 생성 방식 자체가 안전해야 한다.
XSS(Cross-Site Scripting)는 공격자가 넣은 스크립트가 다른 사용자의 브라우저에서 실행되는 취약점이다. 댓글, 닉네임, 검색어, 관리자 페이지의 로그 뷰어처럼 사용자 입력이 HTML에 다시 출력되는 지점에서 자주 발생한다. XSS가 성공하면 공격자는 사용자의 권한으로 화면을 조작하거나 요청을 보낼 수 있다.
방어는 출력 위치에 맞는 이스케이프에서 시작한다. HTML 본문, 속성, URL, JavaScript 문자열은 각각 안전하게 처리하는 방식이 다르다. 프레임워크가 기본적으로 escaping을 제공하더라도 innerHTML, dangerouslySetInnerHTML처럼 HTML을 직접 삽입하는 기능을 쓰면 보호가 깨질 수 있다. 사용자가 작성한 HTML을 허용해야 한다면 허용할 태그와 속성을 제한하는 sanitizer를 사용해야 한다. 추가로 HttpOnly 쿠키는 JavaScript가 세션 쿠키를 읽지 못하게 하고, CSP(Content Security Policy)는 실행 가능한 스크립트 출처를 제한해 피해를 줄일 수 있다.
CSRF(Cross-Site Request Forgery)는 사용자가 로그인한 사이트에 대해 브라우저가 쿠키를 자동으로 보낸다는 성질을 악용한다. 사용자가 은행 사이트에 로그인한 상태에서 공격자 사이트를 방문했는데, 그 페이지가 은행의 송금 API로 요청을 보내면 브라우저는 은행 쿠키를 함께 보낼 수 있다. 서버가 별도 확인 없이 쿠키만 믿으면 원치 않는 작업이 실행된다.
CSRF 방어에는 상태를 바꾸는 작업을 GET으로 처리하지 않기, 예측 불가능한 CSRF 토큰 검증, SameSite 쿠키 설정, Origin 또는 Referer 헤더 확인이 사용된다. OWASP는 서버가 세션과 연결된 토큰을 생성하고 요청마다 검증하는 방식을 대표적인 방어책으로 설명한다4. 단, XSS가 있으면 공격자가 페이지 안에서 토큰을 읽거나 요청을 만들 수 있으므로 CSRF 방어도 우회될 수 있다. 웹 취약점은 서로 독립된 문제가 아니라 연결된 문제다.
비밀번호와 비밀키는 유출을 전제로 설계한다
안전한 시스템은 "절대 유출되지 않는다"가 아니라 "일부가 유출되어도 피해가 제한된다"를 목표로 한다. 비밀번호 저장이 대표적이다. 데이터베이스가 유출되더라도 원문 비밀번호가 바로 노출되지 않도록 해야 한다.
비밀번호 저장의 기본은 다음과 같다.
평문으로 저장하지 않는다.
일반적인 경우 복호화 가능한 암호화로 저장하지 않는다.
사용자마다 고유한 salt를 사용한다.
Argon2id, bcrypt, PBKDF2 같은 검증된 비밀번호 해시 알고리즘을 사용한다.
해시 비용은 서버가 감당 가능한 범위에서 충분히 높게 설정한다.
필요하다면 pepper를 별도 비밀 저장소에 두어 방어층을 추가한다.
여기서 salt는 같은 비밀번호라도 서로 다른 해시가 나오게 만드는 공개 난수다. salt는 비밀일 필요는 없지만 사용자마다 달라야 한다. pepper는 모든 비밀번호에 공통으로 더하는 비밀값이다. pepper는 데이터베이스에 같이 저장하면 의미가 약해지므로 KMS, HSM, 비밀 관리 시스템 같은 별도 위치에 둬야 한다.
비밀키와 토큰 관리도 같은 관점이 필요하다. API 키, 데이터베이스 비밀번호, JWT 서명키, OAuth 클라이언트 시크릿은 소스 코드에 하드코딩하지 않는다. Git 저장소에 한 번 올라간 비밀은 삭제 커밋을 해도 이미 복제되었을 수 있다. 노출이 의심되면 즉시 폐기하고 새 키로 교체해야 한다.
환경 변수는 설정 주입에는 편리하지만 완전한 비밀 금고는 아니다. 프로세스 덤프, 로그, 잘못된 권한의 관리 화면을 통해 노출될 수 있다. 중요한 키는 가능하면 클라우드 KMS, Vault, Secrets Manager 같은 전용 저장소에서 관리하고, 애플리케이션에는 필요한 시점에 최소 권한으로 전달하는 편이 안전하다.
최소 권한은 사고의 폭을 줄인다
최소 권한 원칙은 시스템의 각 구성 요소가 자기 역할에 필요한 권한만 가져야 한다는 원칙이다. 이 원칙은 공격을 완전히 막는 기술은 아니지만, 침해가 발생했을 때 피해 범위를 크게 줄인다.
예를 들어 읽기 전용 배치 작업 계정에는 SELECT 권한만 있으면 된다. 그런데 편의상 운영 데이터베이스의 관리자 계정을 사용하면, 해당 배치 서버가 침해되었을 때 공격자는 테이블 삭제, 사용자 생성, 권한 변경까지 할 수 있다. 파일 업로드 서버가 이미지 저장 버킷 전체 삭제 권한을 가질 필요도 없다. 새 파일을 쓰는 권한과 특정 경로를 읽는 권한만 있으면 충분한 경우가 많다.
애플리케이션 내부에서도 최소 권한은 중요하다. 일반 사용자, 관리자, 운영자, 정산 담당자는 서로 다른 권한을 가져야 한다. 관리자라 해도 모든 행동이 무제한이어서는 안 된다. 대량 다운로드, 권한 변경, 환불, 개인정보 열람 같은 민감 작업에는 추가 승인, 재인증, 감사 로그를 붙이는 것이 좋다.
보안 로그도 설계 대상이다. 누가 언제 어떤 자원에 접근했고, 권한 거부가 얼마나 발생했으며, 어떤 토큰이 폐기되었는지 추적할 수 있어야 한다. 다만 로그에 비밀번호, 전체 카드번호, 인증 토큰을 남기면 로그 시스템이 또 다른 유출 지점이 된다. 로그는 원인을 추적할 만큼 충분해야 하지만, 탈취되었을 때 피해를 키우지 않아야 한다.
개발자가 기억해야 할 기준
보안을 실무 판단으로 바꾸면 다음 질문들이 남는다.
이 입력은 어디에서 왔고, 코드나 쿼리로 해석될 가능성은 없는가?
로그인 여부와 별개로 이 사용자가 이 자원에 접근할 권한이 있는가?
비밀번호와 비밀키는 유출되어도 즉시 원문이 드러나지 않도록 저장되어 있는가?
민감한 데이터는 저장 중과 전송 중에 각각 보호되고 있는가?
각 서비스와 계정은 필요한 최소 권한만 가지고 있는가?
실패했을 때 기본 동작은 허용인가, 거부인가?
좋은 보안 설계는 특별한 기술 하나로 완성되지 않는다. 안전한 기본값, 명시적인 권한 확인, 검증된 암호 기술, 비밀 관리, 로그와 모니터링이 함께 작동해야 한다. 개발자는 모든 공격 기법을 전문가 수준으로 외울 필요는 없다. 하지만 자신이 작성한 코드가 어떤 신뢰 경계를 넘고 있는지, 그 경계에서 무엇을 확인해야 하는지는 알아야 한다. 그것이 보안을 별도 작업이 아니라 개발의 기본 품질로 다루는 출발점이다.
참고
1OWASP Password Storage Cheat Sheet
2OWASP Cryptographic Storage Cheat Sheet
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.