방어적 프로그래밍
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.
1. 개요
방어적 프로그래밍은 소프트웨어 버그를 줄이기 위한 접근 방식으로, 안전한 프로그래밍이라고도 불린다. 이는 잠재적인 공격을 막기 위해 버그를 피하는 것을 목표로 하며, 특히 코드 주입, 서비스 거부 공격과 같은 보안 문제에 대응한다. 방어적 프로그래밍 기법으로는 지능적인 소스 코드 재사용, 정규화, 잠재적 버그에 대한 낮은 허용치 등이 있으며, 데이터 보안의 세 가지 규칙을 따르는 것이 중요하다. 공격적 프로그래밍은 방어적 프로그래밍의 한 범주로, 오류를 방어적으로 처리하지 않고, 프로그램의 자체 데이터는 신뢰하는 방식을 사용한다.
더 읽어볼만한 페이지
- 프로그래밍 원칙 - 블랙박스
블랙 박스는 자극 입력과 출력 반응의 관점에서 시스템을 추상화하여 외부적으로 관찰하는 이론으로, 전자 회로 이론, 사이버네틱스, 시스템 이론 등 다양한 분야에서 활용되며 예측 모델 구축 및 유효성 검증, 그리고 항공기 비행 기록 장치나 함수 교육 도구 등 실생활에도 응용된다. - 프로그래밍 원칙 - 정보 은닉
정보 은닉은 소프트웨어 설계에서 모듈 내부 구현을 숨기고 정의된 인터페이스를 통해서만 상호 작용하도록 하여 모듈 독립성을 높이고 시스템 복잡성을 관리하는 데 중요한 기법으로, 객체 지향 프로그래밍의 캡슐화와 관련된다. - 프로그래밍 패러다임 - 지식 표현
지식 표현은 컴퓨터가 인간의 지식을 이해하고 활용하도록 정보를 구조화하는 기술이며, 표현력과 추론 효율성의 균형, 불확실성 처리 등을 핵심 과제로 다양한 기법과 의미 웹 기술을 활용한다. - 프로그래밍 패러다임 - 의도적 프로그래밍
의도적 프로그래밍은 프로그래머의 의도를 명확히 포착하고 활용하여 소프트웨어 개발 생산성을 향상시키기 위한 프로그래밍 패러다임으로, 트리 기반 저장소를 사용해 코드 의미 구조를 보존하고, WYSIWYG 환경에서 도메인 전문가와 협업하며, 코드 상세 수준 조절 및 자동 문서화를 통해 가독성과 유지보수성을 높이는 데 중점을 둔다.
방어적 프로그래밍 |
---|
2. 안전한 프로그래밍
방어적 프로그래밍은 때때로 컴퓨터 학자들에 의해 소프트웨어 버그를 줄이는 접근을 언급하는 '''안전한 프로그래밍'''(secure programming)으로 불리기도 한다. 소프트웨어 버그는 잠재적으로 해커에 의해 코드 주입, 서비스 거부 공격 또는 다른 공격을 위해 이용당할 수 있다.
안전한 프로그래밍은 일반적인 프로그래밍 습관과 달리, 발생 가능한 모든 오류 상태를 다루려고 시도한다는 점에서 차이가 있다. 즉, 프로그래머는 특정 함수 호출이나 라이브러리가 예상과 다르게, 심지어 악의적으로 동작할 수 있다고 가정하고 코드를 작성해야 한다. 예를 들어, 사용자 입력 길이를 제대로 확인하지 않으면 버퍼 오버플로우와 같은 취약점이 발생할 수 있다. 일부 프로그래머는 사용자가 비정상적으로 긴 입력을 하지 않을 것이라고 가정하며 이를 간과할 수 있다. 하지만 안전한 프로그래밍을 실천하는 프로그래머는 머피의 법칙처럼 알려진 버그가 실제 사용 시에 발생할 수 있음을 인지하고 이를 허용하지 않는다. 이러한 버그는 버퍼 오버플로우 취약점을 이용한 공격으로 이어질 수 있으며, 관련 상세 내용은 보안 코딩 섹션에서 다룬다.
2. 1. 보안 코딩
컴퓨터 보안과 관련된 방어적 프로그래밍의 하위 집합이다. 때때로 컴퓨터 학자들은 버그를 줄이는 이러한 접근 방식을 '''안전한 프로그래밍'''(secure programming)이라고 부르기도 한다. 보안 코딩의 주요 목표는 소프트웨어 버그를 피하는 것이지만, 단순히 소프트웨어의 오작동 가능성을 줄이는 것(안전성)을 넘어 공격 표면(attack surface)을 줄이는 것에 더 큰 중점을 둔다. 즉, 프로그래머는 소프트웨어가 버그를 드러내기 위해 적극적으로 오용될 수 있으며, 발견된 버그가 악의적으로 익스플로잇될 수 있다고 가정해야 한다.예를 들어, 다음 C 코드는 입력 문자열의 길이를 확인하지 않고 `strcpy` 함수를 사용하여 버퍼에 복사한다.
int 위험한_프로그래밍(char *입력) {
char str[1000];
// ...
strcpy(str, 입력); // 입력 복사
// ...
}
만약 `입력` 문자열의 길이가 1000자를 초과하면, `strcpy` 함수는 할당된 버퍼 `str`의 경계를 넘어 데이터를 쓰게 되어 버퍼 오버플로우가 발생한다. 일부 미숙한 프로그래머는 사용자가 그렇게 긴 입력을 하지 않을 것이라고 가정하며 이것이 큰 문제가 아니라고 생각할 수 있다. 하지만 방어적 프로그래밍을 실천하는 프로그래머는 이러한 버그를 용납하지 않을 것이다. 머피의 법칙처럼, 알려진 버그는 실제 사용 중에 발생할 가능성이 높기 때문이다. 이 특정 버그는 버퍼 오버플로우 익스플로잇을 가능하게 하는 심각한 보안 취약점을 보여준다.
보안 코딩 원칙을 적용하여 이 문제를 해결한 예는 다음과 같다.
int 보안_프로그래밍(char *입력) {
char str[1000+1]; // 널 문자를 위한 공간 포함
// ...
// strncpy를 사용하여 버퍼 크기만큼만 복사
strncpy(str, 입력, sizeof(str)); // sizeof(str)은 널 문자를 포함한 전체 크기
// 입력 문자열 길이(strlen(입력))가 버퍼 크기(sizeof(str))보다 크거나 같으면
// strncpy는 널 문자를 추가하지 않을 수 있다.
// 따라서 버퍼의 마지막 문자를 항상 널 문자로 설정하여 문자열 종료를 보장한다.
// 이는 처리할 수 있는 최대 길이로 문자열을 효과적으로 자르는 것과 같다.
str[sizeof(str) - 1] = '\0';
// ...
// 경우에 따라 입력 길이가 너무 길면 프로그램을 명시적으로 중단시키는 방식을 선택할 수도 있다.
}
이 수정된 코드는 `strncpy` 함수를 사용하여 복사할 최대 길이를 버퍼의 전체 크기(`sizeof(str)`)로 제한하고, 버퍼의 마지막 위치(`str[sizeof(str) - 1]`)에 명시적으로 널 문자(`\0`)를 추가하여 문자열이 항상 올바르게 종료되도록 보장한다. 이를 통해 버퍼 오버플로우 취약점을 방지할 수 있다.
3. 공격적 프로그래밍
공격적 프로그래밍은 방어적 프로그래밍의 한 범주로, 특정 종류의 오류는 방어적으로 처리해서는 안 된다는 점을 강조하는 접근 방식이다. 이 관점에서는 프로그램 외부의 제어에서 발생하는 오류(예: 사용자 입력)만 처리 대상으로 삼아야 한다. 반면, 소프트웨어 자체나 프로그램 내부의 데이터는 기본적으로 신뢰해야 한다는 방법론이다. 즉, 내부적인 문제가 발생했을 경우 이를 숨기기보다는 명확히 드러내어 빠르게 수정하는 것을 목표로 한다.
3. 1. 내부 데이터 유효성 신뢰
때로는 방어적 프로그래밍을 과도하게 적용하는 경우가 있다. 예를 들어, 특정 열거형 타입의 값에 따라 다른 문자열을 반환하는 함수를 예로 들 수 있다.다음은 신호등 색깔을 입력받아 해당 색깔의 이름을 문자열로 반환하는 함수의 예시이다. 입력값 `c`가 예상된 값(빨강, 노랑, 초록) 중 하나가 아닐 경우를 대비하여 기본값("black")을 반환하도록 코드를 작성할 수 있다. 이는 잘못된 입력에 대비하는 방어적인 접근 방식이다.
```c
const char* trafficlight_colorname(enum traffic_light_color c) {
switch (c) {
case TRAFFICLIGHT_RED: return "red";
case TRAFFICLIGHT_YELLOW: return "yellow";
case TRAFFICLIGHT_GREEN: return "green";
}
return "black"; // 잘못된 값이 들어오면 "black"을 반환 (방어적)
}
```
하지만 함수 내부로 전달되는 데이터의 유효성을 신뢰하는 접근 방식도 있다. 즉, 이 함수가 호출될 때는 이미 `c`의 값이 유효한 값(빨강, 노랑, 초록 중 하나)임이 보장된다고 가정하는 것이다. 만약 이 가정이 깨진다면, 이는 프로그램의 다른 부분에 심각한 버그가 있다는 신호이므로, 조용히 기본값을 반환하기보다는 프로그램을 즉시 중단시켜 문제를 빠르게 인지하고 수정하는 것이 더 나을 수 있다. 이를 공격적 프로그래밍이라고도 부른다.
아래는 같은 함수를 공격적 프로그래밍 방식으로 작성한 예시이다. `switch` 문에서 모든 유효한 경우를 처리한 후, 그 외의 경우가 발생하면 assert 매크로를 사용하여 프로그램 실행을 중단시킨다. `assert(0)`은 "이 코드 경로에 도달해서는 안 된다"는 것을 명확히 나타낸다.
```c
const char* trafficlight_colorname(enum traffic_light_color c) {
switch (c) {
case TRAFFICLIGHT_RED: return "red";
case TRAFFICLIGHT_YELLOW: return "yellow";
case TRAFFICLIGHT_GREEN: return "green";
}
assert(0); // 이 코드는 실행될 수 없음을 단언 (공격적)
}
```
이처럼 내부 데이터의 유효성을 신뢰하고, 예외적인 상황이 발생했을 때 assert 등을 이용해 빠르게 실패(fail-fast)하도록 만드는 방식은 개발 과정에서 오류를 조기에 발견하고 디버깅하는 데 도움을 줄 수 있다.
3. 2. 소프트웨어 구성 요소 신뢰
소프트웨어 구성 요소를 다룰 때, 해당 구성 요소의 신뢰 수준에 따라 다른 프로그래밍 접근 방식을 취할 수 있다.지나치게 방어적인 접근 방식은 새로운 코드나 외부 구성 요소가 제대로 작동하지 않을 가능성을 염두에 두고, 문제가 발생하면 기존의 안정적인 코드나 다른 대안으로 대체하려고 시도한다. 예를 들어, 새로운 기능(`new_code`)을 도입했을 때 이것이 제대로 작동할지 확신하지 못하여, 호환성 모드(`is_legacy_compatible`)를 확인하거나 새로운 기능 실행 후에도 문제가 감지되면(`new_code(user_config) != OK`) 이전 코드(`old_code`)를 실행하는 방식이다. 이는 안정성을 확보하려는 목적이지만, 코드를 복잡하게 만들고 문제의 근본 원인을 파악하고 해결하는 것을 지연시킬 수 있다.[1]
반면, 공격적 프로그래밍(Aggressive programmingeng) 방식은 새로운 코드나 구성 요소가 기본적으로 올바르게 작동할 것이라고 가정하고 신뢰하는 접근 방식이다. 이 방식에서는 만약 예상치 못한 오류가 발생하면(`new_code(user_config) != OK`), 이를 조용히 처리하거나 우회하는 대신, 명확하게 오류를 보고하고 프로그램을 즉시 종료시킨다. 이는 버그가 숨겨지는 것을 방지하고, 문제가 발생했을 때 개발자가 즉시 인지하고 원인을 찾아 수정하도록 유도하는 효과가 있다. 즉, 문제가 발생하면 이를 회피하기보다는 적극적으로 드러내어 신속한 해결을 추구하는 전략이다.[2]
4. 방어적 프로그래밍 기법
방어적 프로그래밍은 때때로 버그를 줄이는 접근법이라는 의미에서 컴퓨터 학자들에 의해 안전한 프로그래밍(secure programming)이라고도 불린다. 소프트웨어 버그는 잠재적으로 코드 주입, 서비스 거부 공격 또는 다른 공격을 위해 크래커에 의해 악용될 수 있다. 방어적 프로그래밍은 이러한 잠재적 위협에 대비하여 소프트웨어의 안정성과 보안을 강화하는 것을 목표로 한다.
4. 1. 지능적인 소스 코드 재사용
기존에 테스트를 거쳐 작동하는 것으로 알려진 코드를 재사용하면 버그 발생 가능성을 줄일 수 있다.그러나 코드 재사용이 항상 좋은 방법은 아니다. 특히 널리 배포된 기존 코드를 재사용하면, 더 넓은 공격 대상에게 노출될 수 있으며, 재사용된 코드에 포함된 모든 보안 문제나 취약점을 그대로 가져오게 된다.
기존 소스 코드를 사용하기 전에 해당 코드의 모듈(클래스나 함수 등)을 빠르게 검토하면 잠재적인 취약점을 찾아내거나 개발자가 이를 인지하도록 도울 수 있으며, 프로젝트에 사용하기에 적합한지 판단하는 데 도움이 된다. 오래된 소스 코드, 라이브러리, API, 설정 등을 재사용하기 전에는 이것이 현재의 사용 목적에 적합한지, 또는 레거시 문제에 취약하지는 않은지 고려해야 한다.
레거시 문제는 오래된 설계가 오늘날의 요구 사항에 맞춰 작동해야 할 때 발생하며, 특히 오래된 설계가 현재의 요구 사항을 염두에 두고 개발되거나 테스트되지 않았을 때 두드러진다.
많은 소프트웨어 제품이 오래된 레거시 코드와 관련하여 문제를 겪어왔다. 예를 들면 다음과 같다.
- 레거시 코드는 방어적 프로그래밍 원칙 없이 설계되었을 수 있어, 새로 설계된 코드보다 품질이 낮을 수 있다.
- 레거시 코드는 더 이상 유효하지 않은 조건에서 작성되고 테스트되었을 수 있다. 오래된 품질 보증 테스트는 현재 환경에서는 의미가 없을 수 있다.
- * '''예시 1''': 레거시 코드는 ASCII 입력을 기준으로 설계되었으나, 현재는 UTF-8 입력을 사용한다.
- * '''예시 2''': 레거시 코드는 32비트 아키텍처에서 컴파일되고 테스트되었지만, 64비트 아키텍처에서 컴파일하면 새로운 연산 문제(잘못된 부호 검사, 잘못된 타입 변환 등)가 발생할 수 있다.
- * '''예시 3''': 레거시 코드는 오프라인 환경을 가정했지만, 네트워크 연결이 추가되면서 취약해질 수 있다.
- 레거시 코드는 새로운 유형의 보안 문제를 고려하지 않고 작성되었을 수 있다. 예를 들어, 1990년에 작성된 코드는 당시에는 널리 알려지지 않았던 많은 코드 주입 공격에 취약할 가능성이 높다.
레거시 문제의 주요 사례는 다음과 같다.
- BIND 9: 개발자인 폴 빅시(Paul Vixie)와 데이비드 콘래드(David Conrad)는 "BINDv9는 완전한 재작성"이며 "보안이 설계의 핵심 고려 사항이었다"고 발표했다.[2] 이는 보안, 안정성, 확장성 및 새로운 프로토콜 지원을 위해 오래된 레거시 코드를 재작성한 대표적인 사례이다.
- 마이크로소프트 윈도우: Windows 메타파일 취약점(WMF) 및 관련 익스플로잇으로 어려움을 겪었다. 마이크로소프트 보안 대응 센터에 따르면 WMF 기능은 1990년경에 추가되었으며, 이는 "보안 환경이 지금과는 다른 시대였고... 모든 것이 완전히 신뢰받던" 시기였다.[3] 즉, 현재의 보안 기준 하에 개발되지 않았다.
- 오라클: SQL 인젝션이나 권한 상승 같은 보안 문제를 충분히 고려하지 않고 작성된 오래된 소스 코드와 같은 레거시 문제로 어려움을 겪었다. 이로 인해 많은 보안 취약점이 발생했으며, 이를 수정하는 데 시간이 걸리고 불완전한 수정이 이루어지기도 했다. 이는 데이비드 리치필드, 알렉산더 콘브러스트, 세사르 세루도와 같은 보안 전문가들로부터 강한 비판을 받았다.[4][5][6] 또한, 기본 설치 상태(주로 오래된 버전의 레거시 설정)가 오라클 자체의 보안 권장 사항(예: 오라클 데이터베이스 보안 체크리스트)과 일치하지 않는다는 비판도 있다. 이는 많은 애플리케이션이 제대로 작동하기 위해 덜 안전한 레거시 설정을 요구하기 때문에 해결하기 어려운 문제이다.
4. 2. 정규화 (Canonicalization)
악의적인 사용자는 입력값에 대한 다양한 표현식을 사용하여 시스템을 공격할 수 있다. 예를 들어, 특정 파일 `/etc/passwd`에 대한 접근을 차단하도록 프로그램이 설정되어 있더라도, 공격자는 `"/etc/./passwd"`와 같이 파일 이름을 다르게 표현하여 접근을 시도할 수 있다. 이러한 비-표준 입력으로 인해 발생할 수 있는 문제를 방지하기 위해 정규화 라이브러리를 사용할 수 있다. 정규화는 입력값을 일관된 표준 형식으로 변환하여, 다양한 형태의 입력 표현식으로 인한 보안 취약점을 예방하는 데 도움을 준다.4. 3. 잠재적 버그에 대한 낮은 허용치
방어적 프로그래밍은 때때로 컴퓨터 학자들이 버그를 줄이는 접근법이라는 의미에서 '''안전한 프로그래밍'''(secure programming)이라고도 불린다. 소프트웨어 버그는 잠재적으로 코드 주입, 서비스 거부 공격 또는 다른 공격을 위해 크래커에 의해 악용될 수 있다.방어적 프로그래밍과 일반적인 프로그래밍 습관 사이의 주요 차이점은, 방어적 프로그래머는 코드 내 모든 가능한 오류 상태를 처리하려고 시도하며 특정 함수 호출이나 라이브러리가 예상과 다르게, 심지어 적대적으로 동작할 가능성까지 염두에 둔다는 점이다. 반면, 일반적인 접근 방식에서는 이러한 가능성을 간과하는 경우가 많다. 예를 들어, 다음과 같은 코드를 보자.
int risky_programming(char *input){
char str[1000+1]; // 널 종료 문자를 위한 공간 추가
// ...
strcpy(str, input); // input을 str에 복사
// ...
}
이 함수는 `input`의 길이가 1000자를 초과하면 버퍼 오버플로우를 일으켜 프로그램 충돌을 유발할 수 있다. 일부 경험이 부족한 프로그래머는 사용자가 그렇게 긴 입력을 하지 않을 것이라고 가정하며 이를 사소한 문제로 여길 수도 있다. 그러나 방어적 프로그래밍을 실천하는 프로그래머는 이러한 버그를 용납하지 않는다. 머피의 법칙처럼, 프로그램에 알려진 버그가 있다면 결국 실제 사용 중에 문제가 발생할 가능성이 높기 때문이다. 특히 이 버퍼 오버플로우 버그는 취약점 공격에 악용될 수 있는 심각한 보안 취약점을 드러낸다.
이 문제에 대한 방어적인 해결책은 다음과 같다.
int secure_programming(char *input){
char str[1000];
// ...
strncpy(str, input, sizeof(str)); // str의 크기를 초과하지 않도록 input 복사
str[sizeof(str) - 1] = '\0'; // 입력 길이가 배열 크기와 같으면 strncpy가 널 종료를 보장하지 않으므로 수동 추가
// ...
}
결론적으로, 방어적 프로그래밍에서는 문제가 발생할 가능성이 조금이라도 보이는 코드 구조(예: 알려진 취약점과 유사한 패턴)는 단순한 버그를 넘어 잠재적인 보안 결함으로 간주하고 수정해야 한다. 이는 모든 종류의 보안 공격을 예측할 수는 없지만, 알려진 위협에 대비하고 나아가 알려지지 않은 위협까지도 사전에 방지하려는 적극적인 자세를 취하는 것이다.
4. 4. 기타 보안 코딩 방법
가장 흔한 보안 문제 중 하나는 프로그램 입력과 같이 크기가 변하는 데이터에 대해 크기가 고정된 공간을 검증 없이 사용하는 것이다. 이는 버퍼 오버플로우 문제를 일으킬 수 있다. 특히 C 언어에서 문자열 데이터를 다룰 때 자주 발생하는데, 예를 들어 C 라이브러리 함수 중 `gets`와 같이 입력 데이터의 최대 크기를 지정할 수 없는 함수는 절대 사용해서는 안 된다. `scanf`와 같은 함수는 비교적 안전하게 사용할 수 있지만, 사용하기 전에 형식 문자열을 신중하게 선택하고 검증해야 한다.[1]네트워크를 통해 주고받는 중요한 데이터는 반드시 암호화하고 인증해야 한다. 직접 암호화 방식을 만드는 것은 위험하며, 이미 검증된 암호화 방식을 사용하는 것이 안전하다. 순환 중복 검사(CRC)와 같은 기술을 사용하여 메시지를 검사하는 것도 데이터 보호에 도움이 된다.
4. 4. 1. 데이터 보안의 세 가지 규칙
내부 또는 외부에서 제공된 모든 데이터를 처리하는 방법에 대한 세 가지 보안 규칙은 다음과 같다.- 모든 데이터는 폐기해도 좋다고 확인되기 전까지는 중요한 것으로 간주해야 한다. 이는 모든 데이터가 파괴되기 전에 쓸모없는 데이터인지 검증해야 함을 의미한다.
- 모든 데이터는 안전하다고 증명되지 않는 한 오염된 것으로 간주한다. 이는 데이터의 무결성이 검증되기 전까지는, 해당 데이터가 시스템의 다른 부분(런타임 환경)에 영향을 주지 않도록 격리된 방식으로 처리해야 함을 의미한다.
- 모든 코드는 안전하다고 증명되지 않는 한 안전하지 않다고 간주한다. 이는 코드에 버그나 정의되지 않은 동작과 같은 문제가 있을 수 있으며, 이로 인해 SQL 주입과 같은 공격에 시스템이 노출될 수 있음을 항상 염두에 두어야 한다는 의미이다. 특히 사용자 공간의 코드는 안전성을 완전히 증명하기 어려우므로, "클라이언트를 절대 신뢰하지 마라"는 원칙을 따르는 것이 중요하다.
4. 4. 2. 추가 정보
참조
[1]
간행물
6 - Technique to Manage Software Safety
https://www.scienced[...]
Elsevier
2016-01-01
[2]
웹사이트
fogo archive: Paul Vixie and David Conrad on BINDv9 and Internet Security by Gerald Oskoboiny
[3]
뉴스
Looking at the WMF issue, how did it get there?
http://blogs.technet[...]
2018-10-27
[4]
웹사이트
Bugtraq: Oracle, where are the patches???
http://seclists.org/[...]
2018-10-27
[5]
웹사이트
Bugtraq: RE: Oracle, where are the patches???
http://seclists.org/[...]
2018-10-27
[6]
웹사이트
Bugtraq: Re: [Full-disclosure] RE: Oracle, where are the patches???
http://seclists.org/[...]
2018-10-27
본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.
문의하기 : help@durumis.com