소프트웨어 개발보안 가이드 분석(2021) : Null Pointer 역참조
컴퓨터 프로그래밍에서, 특히 Java와 같은 언어에서 Null Pointer Dereference (NPD) 취약점은 주의 깊게 다루어야 할 중요한 이슈입니다. 이 취약점은 프로그램이 null 포인터를 참조할 때 발생하며, 이로 인해 프로그램이 예기치 않게 중단될 수 있습니다. 더 심각한 경우, 공격자는 이 취약점을 이용하여 서비스 거부(DoS) 공격을 수행하거나 민감한 정보에 접근할 수도 있습니다.
개발 환경에서 사용하는 객체의 Null 상황은 자주 마주할 수 있는 내용이지만, 해당 공격은 심각한 보안사고로 이어질 수 있으므로 충분한 주의가 필요합니다. 실제로 최근 NGINX Plus에서 발견된 CVE-2024-24989 취약점이 대표적인 사례로 소개될 수 있습니다. 이는 NULL 포인터 역참조 취약점(CWE-476)에 속하는데, 특정 조건 하에서 공격자가 특별히 조작된 요청을 보내 NGINX Plus 서버를 크래시시킬 수 있게 만드는 문제입니다. 이를 통해 서비스 거부(DoS) 공격이 가능하며, 원격에서도 이 취약점을 이용할 수 있습니다. 공격자는 특별히 조작된 요청을 보내 NGINX Plus 서버를 크래시시켜 서비스 거부(DoS) 공격을 유발할 수 있었습니다.
Null Pointer 역참조란?
널 포인터(Null Pointer) 역참조는 '일반적으로 그 객체가 널(Null)이 될 수 없다'라고 하는 가정을 위반했을 때 발생한다.
공격자가 의도적으로 널 포인터 역참조를 발생시키는 경우, 그 결과 발생하는 예외 상황을 이용하여 추후의 공격을 계획하는 데 사용될 수 있다.
소프트웨어 개발보안가이드(2021), 한국인터넷진흥원
Null Pointer 역참조는 프로그램이 null 값을 참조하는, 존재하지 않은 메모리 위치에 접근하려는 시도를 말합니다. 이는 널 포인터(Null Pointer)를 역참조했을 때 발생하며, 프로그램 충돌, 데이터 손상, 심지어 공격자의 침입까지 허용할 수 있는 심각한 보안 문제를 발생시킬 수 있으므로 소프트웨어 개발 및 유지 관리 과정에서 NULL 포인터 역참조 취약점에 대한 지속적인 주의와 대응이 필요합니다. 하단에서 더 자세히 논의할 내용이지만, 개발자와 소스코드 진단 전문가는 해당 취약점 해결을 위해 다음과 같은 대응 방안을 고려할 수 있습니다.
- 코드 리뷰와 정적 분석: 정적 분석 도구를 사용하여 코드 내의 NULL 포인터 역참조 취약점을 자동으로 감지하고 수정할 수 있습니다.
- Optional 클래스와 같은 안전한 프로그래밍 패턴 사용: Java 8 이상에서는 Optional 클래스를 사용하여 null 가능성을 명시적으로 다루고, null 검사를 보다 우아하게 처리할 수 있습니다.
- 예외 처리: 예외 처리 메커니즘을 통해 예상치 못한 null 값에 대한 처리를 구현할 수 있습니다. 그러나 예외 처리를 정상적인 프로그램 흐름 제어에 사용하는 것은 피해야 합니다.
공격 메커니즘
우선 Null Pointer 역참조 공격 메커니즘을 먼저 분석해보면, 주로 공격자는 웹 어플리케이션의 정상적인 서비스 메뉴 혹은 기능 요청을 통해 널 포인터 예외(NullPointerException)을 발생시키고, 예상치못한 오류 상황으로 인해 발생한 정보 노출 등의 데이터를 악용하여 시스템을 공격합니다. 이는 널 포인터 예외를 올바르게 처리하지 않은 경우에 발생하여 다음과 같은 결과를 낳을 수 있습니다.
1.공격자가 Null Pointer 역참조를 수행할 수 있는 방법은 다양합니다.
- 입력 검증 부족 : 사용자 입력을 제대로 검증하지 않아 널 값을 가진 변수를 만들도록 유도합니다.
- 객체 참조 변경 : 공격자는 널 값으로 객체 참조를 변경하여 악성 코드를 실행하도록 조작합니다.
- 메모리 할당 오류 : 메모리 할당 오류를 일으켜 널 포인터를 생성합니다.
2. 결과적으로 Null Pointer 공격을 통해 다음과 같은 결과를 초래할 수 있습니다.
- 프로그램 충돌 : 프로그램이 비정상적으로 종료됩니다.
- DoS : 공격자는 널 포인터 예외를 유발하여 시스템을 다운시키거나 서비스의 가용성을 저해시킵니다.
- 데이터 손상 : 중요한 데이터가 손상되거나 유출될 수 있습니다.
- 시스템 장악 : 널 포인터 예외를 적절히 처리하지 않아 공격자는 악의적인 코드를 실행할 수 있습니다. 공격자가 널 포인터가 발생할 수 있는 부분에 악의적인 코드를 입력하여 예상치 못한 결과를 만들어 냅니다.
취약한 웹 애플리케이션의 예
public static void main(String[] args) {
1 String name = null;
2 if (name.length() > 0) {
3 System.out.println("사용자 이름: " + name);
}
4 System.out.println("사용자 정보: " + name.toUpperCase());
}
- 1 : 예시를 위하여 name에 null로 선언하였습니다. 즉, name 변수는 현재 어떤 문자열도 참조하지 않습니다.
- 2 : name의 길이를 검사하는데, name이 null로 length() 메서드를 호출하면 Null Pointer 예외가 발생합니다.
- 3 : name의 변수 값이 출력되어야 하지만, Null Pointer 예외가 발생하여 출력되지 않거나 프로그램이 비정상적으로 종료될 수 있습니다.
- 4 : legth 메서드와 마찬가지로 name이 null 일 경우에 toUpperCase 메서드를 호출하면 Null Pointer 예외가 발생할 수 있습니다.
- 결과적으로 Null Pointer 예외가 발생하여 프로그램이 비정상 종료되거나 아무런 출력이 되지 않습니다. 따라서 Null Pointer 예외를 방지하기 위해 null 값을 체크해줘야 합니다.
시큐어코딩 적용 방법
public static void main(String[] args) {
String name = null;
1 if (name != null && name.length() > 0) {
System.out.println("사용자 이름: " + name);
} else {
System.out.println("사용자 이름이 null이거나 빈 문자열입니다.");
}
2 if (name != null) {
System.out.println("사용자 정보: " + name.toUpperCase());
} else {
System.out.println("사용자 이름이 null입니다.");
}
}
- 1 : null 값을 검증하는 조건문으로 name 변수가 null이 아니고 그 길이가 0보다 큰지를 검사합니다. 즉, name이 null이 아니고 비어있지 않은 경우에만 실행됩니다. Null Pointer 예외는 name이 null일 경우 length 메서드를 실행하여 발생하지만 null인 경우를 체크하기 때문에 예외가 발생하지 않습니다.
- 2 : 마찬가지로 name이 null 인지를 검사하고 있습니다. name이 null이 아닌 경우에만 toUpperCase 메서드를 실행하기 때문에 Null Pointer 예외는 발생하지 않습니다.
- 결과적으로 null 값을 모두 검증하며 객체(name)를 참조하고 있기 때문에 Null Pointer 역참조 취약점에 대응할 수 있습니다.
※ toUpperCase, length 모두 Null Pointer 역참조 취약점을 보여드리기 위해 사용하였습니다. 다른 Null Pointer 참조 예외를 발생시키는 메서드들도 있으며, 이 메서드들은 null 값을 가진 객체에 호출되면 Null Pointer 참조 예외를 발생시킵니다.
Optional 클래스를 활용한 시큐어코딩 방안
Java 8 이후로 Optional 클래스를 사용하면 null을 더 창의적으로 처리할 수 있습니다. Optional은 null 가능성이 있는 값을 캡슐화하고, 값이 없을 경우 기본 동작을 정의할 수 있는 방법을 제공합니다. 아래는 Optional을 사용한 개선된 예제입니다.
import java.util.Optional;
public class SecureCodingExample {
public static void main(String[] args) {
// 예제를 위한 사용자 이름 null 설정
String name = null;
// Optional 객체 사용
Optional<String> optionalName = Optional.ofNullable(name);
// ifPresent() 메소드를 사용하여 값이 있는 경우에만 동작 수행
optionalName.ifPresent(userName -> System.out.println("사용자 이름: " + userName));
// isPresent() 메소드와 get() 메소드를 사용하지 않고, orElse()로 대체 값 제공
System.out.println("사용자 이름(기본값 포함): " + optionalName.orElse("기본 사용자"));
// map() 메소드를 사용하여 값이 있는 경우에만 함수 적용
String userInfo = optionalName.map(String::toUpperCase).orElse("사용자 이름이 null입니다.");
System.out.println("사용자 정보: " + userInfo);
}
}
이 코드는 Optional 클래스를 사용하여 null 검사를 더 명시적이고 유연하게 수행합니다. ifPresent(), orElse(), map() 같은 메소드를 사용하여 null 체크를 간소화하고, 코드 읽기를 더 쉽게 만듭니다. 또한, Optional을 사용함으로써 코드의 의도를 더 명확하게 표현할 수 있고, 다른 개발자가 코드를 이해하기 쉽게 만듭니다.
NullPointerException을 활용한 시큐어코딩
해당 취약점에 대한 검출결과가 무지막지(?)하게 많이 나올 경우, NullPointerException을 캐치하여 사용자에게 적절한 메시지를 출력하여 프로그램이 강제 종료되지 않도록 하는 방법으로 조치가 이루어지기도 합니다. 하지만 이러한 방식은 예외적인 상황에서만 사용되어야 하며, 정상적인 프로그램의 흐름 제어나 비즈니스 로직 구현을 위해 사용되어서는 안 됩니다.
public class NullPointerExceptionHandling {
public static void main(String[] args) {
try {
String name = null;
// 의도적으로 NullPointerException을 발생시킴
System.out.println(name.length());
} catch (NullPointerException e) {
// NullPointerException을 캐치하고 적절한 메시지 출력
System.out.println("null 값을 참조하려고 했습니다. 적절한 값으로 초기화하세요.");
}
}
}
해당 시큐어코딩 방안을 통해 단편적으로는 취약점을 방지할 수 있겠지만, NullPointerException을 활용한 처리 방법은 일반적으로 권장되지 않습니다. 그 이유는 예외 처리가 프로그램의 정상적인 제어 흐름을 위한 메커니즘이 아니기 때문입니다. 예외 처리는 예상치 못한 상황이나 에러 상황을 처리하기 위한 것이지, 정상적인 프로그램 로직의 일부로 사용하기 위한 것이 아닙니다. NullPointerException을 의도적으로 활용하는 것은 코드의 가독성을 떨어뜨리고, 유지보수를 어렵게 만들며, 성능 저하를 일으킬 수 있으므로, 정상적인 서비스 운영과 개발환경의 특이성을 충분히 고려하셔서 작업을 위해 반드시 필요한 경우에만 적용할 것을 권장 드립니다.