출제기준 세세항목: 암호 알고리즘 · 코드오류 · 캡슐화 · API 오용
시큐어 코딩은 해킹에 쓰일 수 있는 빈틈, 즉 보안 약점이 코드에 생기지 않도록 정해진 규칙을 지켜 프로그램을 짜는 것입니다. 보안 약점은 그대로 두면 실제 공격에 쓰이는 취약점이 됩니다. 문을 잠그는 습관을 처음부터 들이는 것이 도둑이 든 뒤 자물쇠를 다는 것보다 훨씬 싸게 먹힌다는 생각입니다.
구현 단계의 보안 약점은 일곱 가지 유형으로 나눕니다. 입력데이터 검증 및 표현, 보안기능, 시간 및 상태, 에러처리, 코드오류, 캡슐화, API 오용입니다. 이름이 그대로 단답형 답이 되므로 일곱 개를 순서대로 말할 수 있을 만큼 외워 두세요. 설계 단계의 네 가지 항목(입력데이터 검증 및 표현, 보안기능, 에러처리, 세션통제)과는 별개의 목록입니다. 입력데이터 검증 및 표현·보안기능·에러처리는 이름이 같지만, 세션통제는 구현 목록에 없고 시간 및 상태·코드오류·캡슐화·API 오용은 설계 목록에 없습니다.
약점을 찾는 방법도 함께 기억해 둡니다. 소스 코드를 실행하지 않고 도구로 훑어 약점 패턴을 찾는 정적 분석, 프로그램을 실제로 실행한 상태에서 공격을 흉내 내 반응을 보는 동적 분석이 있습니다. 실무에서는 코드를 저장소에 올릴 때마다 정적 분석 도구가 자동으로 돌도록 해 두는 경우가 많습니다.
가장 많은 약점이 몰려 있는 유형입니다. 원칙은 하나, 사용자 입력·파일·외부 시스템 응답처럼 바깥에서 들어온 값은 검사하기 전까지 믿지 않는다는 것입니다. 검사는 허용할 값의 목록이나 형식을 정해 두고 그것만 통과시키는 화이트리스트 방식이, 막을 값만 골라 거르는 블랙리스트 방식보다 안전합니다. 공격자는 늘 목록에 없는 새 값을 찾아내기 때문입니다.
대표 약점과 대응을 짝지어 봅니다. SQL 인젝션은 입력값을 질의문에 문자열로 이어 붙여서 생기므로, 질의문의 틀을 먼저 만들고 값은 자리 표시자에 따로 넣는 매개변수화된 질의(Prepared Statement)로 막습니다. 크로스 사이트 스크립팅(XSS)은 입력값을 화면에 그대로 출력해서 생기므로, 출력할 때 <, > 같은 특수문자를 HTML 엔티티로 바꿔 스크립트로 해석되지 않게 합니다. 운영체제 명령어 삽입은 입력값이 시스템 명령에 붙어 실행되는 것이라, 명령 실행에 입력값을 쓰지 않거나 허용된 값만 쓰게 합니다.
경로 조작은 파일 이름 입력에 '../' 같은 값을 넣어 허락되지 않은 디렉터리의 파일에 접근하는 약점이라, 경로 구분 문자를 걸러 냅니다. 위험한 형식의 파일 업로드는 서버에서 실행될 수 있는 스크립트 파일을 올리는 것이라, 확장자와 크기를 제한하고 업로드 폴더에서는 실행을 막습니다. 신뢰되지 않는 주소로의 자동 연결(오픈 리다이렉트)은 이동할 주소를 입력값으로 받아 피싱 사이트로 보내는 것이라, 이동 가능한 주소를 목록으로 정해 둡니다. 사이트 간 요청 위조(CSRF)도 이 유형에 들어가며, 요청마다 예측할 수 없는 토큰을 넣어 확인합니다.
메모리를 직접 다루는 언어에서는 버퍼 오버플로가 대표적입니다. 정해진 크기의 메모리 공간보다 긴 입력을 넣어 옆 영역을 덮어쓰고, 심하면 공격자의 코드를 실행시킵니다. 입력 길이를 검사하고 길이 제한이 있는 함수를 씁니다. 숫자가 표현 범위를 넘어 엉뚱한 값이 되는 정수형 오버플로, 사용자 입력을 출력 형식 문자열로 그대로 써서 생기는 포맷 스트링 삽입도 이 유형입니다.
보안기능 유형은 인증·접근제어·암호화 같은 보안 기능을 아예 빠뜨리거나 잘못 만든 경우입니다. 로그인 없이 중요한 기능을 쓸 수 있게 둔 것(적절한 인증 없는 중요 기능 허용), 권한 확인을 빠뜨린 것(부적절한 인가), 비밀번호나 암호키를 소스 코드에 직접 적어 둔 것(하드코드된 중요 정보), 로그인 실패를 무제한 허용한 것(반복된 인증 시도 제한 기능 부재)이 대표적입니다.
암호를 쓰더라도 잘못 쓰면 이 유형에 들어갑니다. 이미 깨진 DES·MD5 같은 취약한 암호 알고리즘을 쓰는 것, 키 길이가 너무 짧은 것, 예측 가능한 난수로 세션 ID나 토큰을 만드는 것, 솔트 없이 해시만 한 것, 개인정보나 비밀번호를 평문으로 저장하거나 전송하는 것이 모두 보안기능 약점입니다. 난수가 보안에 쓰일 때는 일반 난수 함수가 아니라 암호학적으로 안전한 난수 생성기를 써야 합니다.
시간 및 상태 유형은 여러 작업이 동시에 돌거나 순서대로 실행될 때 생기는 문제입니다. 검사 시점과 사용 시점(TOCTOU, Time Of Check to Time Of Use) 경쟁 조건은 '파일이 있는지 확인한 순간'과 '그 파일을 실제로 여는 순간' 사이에 다른 프로세스가 파일을 바꿔치기해 생기는 약점입니다. 공유 자원에 동기화(잠금)를 걸어 막습니다.
종료되지 않는 반복문이나 재귀 함수도 시간 및 상태 유형입니다. 끝나는 조건이 없거나 잘못되면 자원을 다 써 버려 서비스가 멈춥니다. 재귀 함수에는 반드시 멈추는 조건을 두고, 반복 횟수에 상한을 둡니다.
에러처리 유형은 오류를 다루는 방식의 실수입니다. 오류 메시지에 스택 트레이스, 서버 경로, DB 테이블 이름 같은 내부 정보를 그대로 보여 주면 공격자에게 지도를 건네는 셈입니다(오류 메시지 정보 노출). 예외를 잡아 놓고 아무 처리도 하지 않는 빈 catch 블록(오류 상황 대응 부재), 모든 예외를 한 번에 뭉뚱그려 잡는 것(부적절한 예외 처리)도 여기에 들어갑니다. 사용자에게는 일반적인 안내만 보여 주고, 자세한 내용은 서버 로그에 남깁니다.
코드오류 유형은 프로그래머가 흔히 하는 코딩 실수가 약점이 되는 경우입니다. 값이 없는(null) 객체를 그대로 쓰려다 프로그램이 죽는 널 포인터 역참조, 파일이나 DB 연결을 쓰고 닫지 않는 부적절한 자원 해제, 이미 해제한 메모리를 다시 쓰는 해제된 자원 사용, 값을 넣지 않은 변수를 쓰는 초기화되지 않은 변수 사용, 믿을 수 없는 데이터를 검사 없이 객체로 되살리는 신뢰할 수 없는 데이터의 역직렬화가 대표적입니다.
캡슐화 유형은 감춰야 할 데이터나 기능이 밖으로 드러나는 경우입니다. 여러 사용자가 함께 쓰는 객체의 멤버 변수에 사용자 정보를 담아 다른 사람의 세션에 보이게 되는 잘못된 세션에 의한 데이터 정보 노출, 개발 중에 넣은 디버그 코드를 지우지 않고 배포한 제거되지 않고 남은 디버그 코드, private으로 선언한 배열을 public 메서드가 그대로 돌려줘 밖에서 고칠 수 있게 되는 경우가 있습니다. 배열은 복사본을 돌려주는 식으로 막습니다.
API 오용 유형은 라이브러리나 시스템 기능을 잘못된 방식으로 쓰는 경우입니다. 접속한 상대를 믿을지 DNS 이름만 보고 판단하는 DNS lookup에 의존한 보안 결정은, DNS 스푸핑으로 이름을 속이면 그대로 뚫립니다. 입력 길이를 검사하지 않는 C 언어의 gets처럼 위험하다고 알려진 함수를 쓰는 취약한 API 사용도 이 유형이며, 길이를 제한하는 안전한 대체 함수로 바꿔 씁니다.
비밀번호는 서버에 원문이나 되돌릴 수 있는 암호문으로 저장하지 않습니다. 서버도 원래 비밀번호를 알 필요가 없기 때문입니다. 가입할 때 비밀번호를 해시해서 저장하고, 로그인할 때 입력값을 같은 방법으로 해시해 저장된 값과 비교합니다. 이렇게 하면 DB가 통째로 유출되어도 원래 비밀번호가 바로 드러나지는 않습니다.
해시만으로는 부족합니다. 흔한 비밀번호의 해시값을 미리 계산해 표로 만들어 둔 레인보우 테이블로 역추적할 수 있기 때문입니다. 그래서 사용자마다 다른 무작위 값인 솔트(Salt)를 비밀번호에 붙여 해시합니다. 같은 비밀번호를 쓰는 두 사람도 저장된 값이 달라지고, 미리 만든 표가 쓸모없어집니다. 여기에 해시를 수천 번 이상 반복해 한 번 계산하는 데 시간이 걸리게 만드는 키 스트레칭을 더하면, 무작정 대입하는 공격이 훨씬 느려집니다. PBKDF2, bcrypt, scrypt, Argon2가 이런 목적의 알고리즘입니다.
로그인에 성공하면 서버는 세션 ID를 발급해 그 사용자를 알아봅니다. 세션 ID는 예측할 수 없는 충분히 긴 난수로 만들고, 로그인에 성공한 순간 새로 발급해야 합니다. 로그인 전에 공격자가 심어 둔 세션 ID를 그대로 쓰게 되는 세션 고정 공격을 막기 위해서입니다. 일정 시간 동작이 없으면 세션을 끊고(타임아웃), 로그아웃하면 서버에서 세션을 확실히 없앱니다.
세션 ID를 담는 쿠키에는 보호 속성을 붙입니다. HttpOnly는 자바스크립트가 쿠키를 읽지 못하게 해 XSS로 세션을 훔치기 어렵게 하고, Secure는 암호화된 HTTPS 연결에서만 쿠키를 보내게 합니다. 로그인 실패가 일정 횟수를 넘으면 계정을 잠시 잠그고, 중요한 작업 전에는 비밀번호를 다시 묻는 재인증을 두는 것도 함께 쓰는 대책입니다.
먼저 고를 수 있는 암호 알고리즘을 세 갈래로 다시 짚어 둡니다. 대칭키 알고리즘은 암호화와 복호화에 같은 키를 쓰며 DES, AES, SEED, ARIA가 여기에 속합니다. 비대칭키(공개키) 알고리즘은 공개키와 개인키 한 쌍을 쓰며 RSA, ECC가 대표입니다. 해시 알고리즘은 되돌릴 수 없는 고정 길이 값을 만들며 MD5, SHA 계열이 있습니다. 어떤 자리에 어떤 갈래를 쓰는지가 구현 단계의 판단입니다.
암호화는 데이터가 움직일 때와 머물러 있을 때 두 곳에서 생각합니다. 움직이는 데이터, 즉 브라우저와 서버 사이처럼 네트워크를 지나는 구간은 SSL/TLS로 암호화합니다. TLS는 SSL의 후속 표준으로, 지금은 오래된 SSL 버전을 쓰지 않고 TLS를 씁니다. 웹에서 TLS를 쓰는 것이 HTTPS입니다. 전송 구간을 암호화하지 않으면 스니핑만으로 비밀번호와 개인정보가 그대로 보입니다.
TLS는 앞에서 본 두 암호를 섞어 씁니다. 처음 연결할 때 인증서와 비대칭키 기술로 서버가 진짜인지 확인하고 대칭키를 안전하게 나눠 가진 뒤, 실제 데이터는 빠른 대칭키로 암호화해 주고받습니다. 이렇게 비대칭키로 키를 나누고 대칭키로 데이터를 처리하는 방식을 하이브리드 암호라고 부르기도 합니다.
머물러 있는 데이터, 즉 DB나 파일에 저장된 데이터 중 주민등록번호·계좌번호 같은 중요 정보는 AES 같은 안전한 대칭키 알고리즘으로 암호화해 저장합니다. 나중에 다시 원래 값이 필요한 정보이므로 해시가 아니라 복호화할 수 있는 암호화를 씁니다. 반대로 비밀번호는 원래 값이 필요 없으니 해시와 솔트로 저장한다는 차이를 구별해 두세요.
암호화의 안전성은 결국 키를 얼마나 잘 지키느냐에 달려 있습니다. 암호키를 소스 코드나 설정 파일에 그대로 적어 두면(하드코딩) 코드가 새는 순간 암호화가 무의미해집니다. 키는 별도의 키 관리 시스템이나 접근이 제한된 저장소에 두고, 주기적으로 바꾸며, 데이터를 암호화하는 키와 그 키를 다시 암호화하는 키를 나눠 관리합니다.
아무리 잘 막아도 사고는 날 수 있으므로, 누가 언제 무엇을 했고 결과가 어땠는지 남기는 로그가 필요합니다. 로그가 있어야 사고 뒤에 원인을 추적하고 책임을 따질 수 있는데, 이를 책임 추적성이라고 합니다. 로그인 성공·실패, 권한 변경, 중요 데이터 조회·수정 같은 사건을 기록하되, 비밀번호나 카드번호 같은 중요 정보는 로그에 그대로 적지 않습니다. 로그는 공격자가 흔적을 지우지 못하도록 별도 서버에 모으고 접근을 제한하며, 여러 서버의 기록을 맞춰 볼 수 있게 시간을 동기화해 둡니다.
네트워크 쪽 보안 도구도 이름과 역할을 짝지어 둡니다. 방화벽(Firewall)은 내부망과 외부망 경계에서 미리 정한 규칙(출발지·목적지 IP, 포트 번호 등)에 따라 트래픽을 통과시키거나 막는 문지기입니다. 침입 탐지 시스템(IDS, Intrusion Detection System)은 트래픽이나 시스템 활동을 지켜보다가 공격으로 보이는 것을 찾아 관리자에게 알립니다. 알려진 공격 패턴(시그니처)과 비교하는 오용 탐지와, 평소와 다른 이상한 행동을 찾는 이상 탐지로 나뉩니다.
침입 방지 시스템(IPS, Intrusion Prevention System)은 트래픽이 지나가는 길목에 직접 놓여, 공격을 탐지하면 알리는 데서 그치지 않고 그 패킷이나 연결을 즉시 차단합니다. IDS가 경보기라면 IPS는 경보와 함께 문을 닫는 경비원입니다. 웹 방화벽(WAF, Web Application Firewall)은 일반 방화벽이 보지 않는 웹 요청의 내용까지 살펴 SQL 인젝션, XSS 같은 웹 애플리케이션 공격을 막습니다.
이 밖에 공용 인터넷 위에 암호화된 전용 통로를 만들어 원격지와 안전하게 통신하게 하는 VPN(가상 사설망), 보안 조건을 갖춘 단말만 내부망에 접속하게 하는 NAC(네트워크 접근 제어), 중요 정보가 메일·USB 등으로 밖으로 새는 것을 막는 DLP(데이터 유출 방지), 여러 장비의 로그와 경보를 한곳에 모아 분석하는 SIEM(보안 정보 및 이벤트 관리), 방화벽·IDS·안티바이러스 등 여러 기능을 장비 하나에 묶은 UTM(통합 위협 관리)이 있습니다. 일부러 허술해 보이는 시스템을 두어 공격자를 끌어들이고 수법을 관찰하는 허니팟도 함께 알아 두세요.
상품 검색 기능이 검색어를 질의문 문자열 뒤에 그대로 이어 붙이고 있었습니다. 정적 분석 도구가 이를 입력데이터 검증 및 표현 유형의 SQL 인젝션 약점으로 표시했고, 개발자는 질의문을 Prepared Statement로 바꿔 검색어가 자리 표시자에 값으로만 들어가게 고쳤습니다. 이어서 검색어 길이를 50자로 제한하는 서버 측 검증도 추가했습니다.
다른 회사의 DB 유출 소식을 듣고 자사 회원 DB를 점검해 보니, 비밀번호가 솔트 없이 MD5로만 해시되어 있었습니다. 취약한 해시 사용과 솔트 없는 해시라는 두 가지 보안기능 약점입니다. 회원이 다음에 로그인할 때 입력한 비밀번호를 사용자별 솔트와 bcrypt로 다시 해시해 저장하도록 바꾸고, 일정 기간이 지나도 바뀌지 않은 계정에는 비밀번호 재설정을 안내했습니다.
쇼핑몰 서버 앞에 방화벽을 두어 웹 서비스용 포트(443)만 열고 나머지는 막았습니다. 그 뒤에 IPS를 두어 SYN 플러딩 같은 네트워크 공격을 실시간으로 끊고, 웹 서버 바로 앞에는 WAF를 두어 주문 페이지로 들어오는 XSS·SQL 인젝션 시도를 걸렀습니다. 각 장비의 경보와 웹 서버 로그는 SIEM으로 모아 한 화면에서 보게 했습니다.