설명
OGNL(Object-Graph Navigation Language)은 객체의 속성을 탐색하는 기능뿐 아니라 메서드 호출, 연산자, 컬렉션 처리, 평가 컨텍스트에 노출된 객체 접근도 지원하는 표현 언어입니다. Apache Struts는 값 스택과 여러 내부 기능에서 OGNL을 사용합니다.
외부에서 제어할 수 있는 문자열을 OGNL 표현식으로 구문 분석하거나 평가하면 공격자가 애플리케이션이 의도하지 않은 객체와 속성에 접근할 수 있습니다. 실제 영향은 루트 객체, 평가 컨텍스트, 허용된 클래스와 멤버, Struts 보안 설정에 따라 달라지며 데이터 노출·변경부터 코드 실행까지 이어질 수 있습니다.
parseExpression이나 compile로 구문 분석한 결과도 신뢰할 수 없는 표현식입니다. 구문 분석이 성공했다는 사실은 접근 권한이나 이후 평가의 안전성을 보장하지 않습니다.
잠재적 영향
- 데이터 노출 또는 변경: 표현식이 접근 가능한 객체 그래프의 속성을 읽거나 변경할 수 있습니다.
- 보안 로직 우회: 애플리케이션이 표현식 결과를 권한, 라우팅 또는 정책 결정에 사용하면 공격자가 해당 결정을 조작할 수 있습니다.
- 코드 실행: 위험한 객체, 메서드, 클래스 또는 컨텍스트 접근이 허용된 구성에서는 애플리케이션 권한으로 코드를 실행할 수 있습니다.
- 서비스 거부: 복잡하거나 반복적인 표현식이 과도한 CPU, 메모리 또는 객체 접근을 유발할 수 있습니다.
해결 방법
가장 중요한 원칙은 표현식 문자열은 애플리케이션이 소유하고, 외부 입력은 데이터로만 전달하는 것입니다.
- 검토된 고정 표현식만 평가합니다. 요청값을 표현식 문자열이나 강제/이중 평가 구문에 이어 붙이지 않습니다.
- 사용자가 필드나 동작을 선택해야 한다면 외부 키를 유한한 서버 소유 매핑을 통해 상수 표현식으로 변환하고, 알 수 없는 키는 거부합니다.
- 신뢰할 수 없는 값은 루트 객체, 평가 컨텍스트, 바인딩 또는 속성 값으로 전달합니다.
- 정규식, 이스케이프 처리, 문자 제거 또는 이름이
sanitize인 도우미만으로 임의의 OGNL 표현식을 안전하게 만들 수 있다고 가정하지 않습니다. 예를 들어 영문자·숫자·밑줄만 허용해도 공격자가 서버 객체의 속성 이름을 선택하는 문제는 남습니다. - Apache Struts를 최신 지원 버전으로 유지하고 Struts가 관리하는 OGNL 의존성을 사용합니다. 아래 Struts 7.3.0 및 OGNL 3.4.12 자료는 버전별 참고 자료입니다. 배포할 때는 지원 상태와 보안 업데이트를 확인하세요.
예시
변경 전
다음 코드는 요청에서 받은 문자열 전체를 OGNL 표현식으로 평가합니다.
import jakarta.servlet.http.HttpServletRequest;
import ognl.Ognl;
import ognl.OgnlException;
final class VulnerableOgnlExample {
static Object readField(HttpServletRequest request, UserProfile profile)
throws OgnlException {
String requestExpression = request.getParameter("expression");
return Ognl.getValue(requestExpression, profile);
}
static final class UserProfile {
private final String displayName;
private final String email;
UserProfile(String displayName, String email) {
this.displayName = displayName;
this.email = email;
}
public String getDisplayName() {
return displayName;
}
public String getEmail() {
return email;
}
}
}
requestExpression에 단순 속성 이름이 아니라 메서드 호출, 컨텍스트 참조 또는 다른 OGNL 구문이 들어올 수 있으므로, 공격자가 평가할 표현식 자체를 선택합니다.
변경 후
외부 값은 표현식이 아니라 제한된 비즈니스 키로 취급하고, 각 키를 서버가 소유한 상수 표현식으로 매핑합니다. switch 표현식을 지원하는 Java 버전의 예시이며, 프로필과 필드에 대한 접근 권한 및 오류 응답 처리는 별도로 구성하세요.
import jakarta.servlet.http.HttpServletRequest;
import ognl.Ognl;
import ognl.OgnlException;
final class SafeOgnlExample {
static Object readField(HttpServletRequest request, UserProfile profile)
throws OgnlException {
String requestedField = request.getParameter("field");
if (requestedField == null) {
throw new IllegalArgumentException("Missing field");
}
String fixedExpression = switch (requestedField) {
case "display-name" -> "displayName";
case "email" -> "email";
default -> throw new IllegalArgumentException("Unsupported field");
};
return Ognl.getValue(fixedExpression, profile);
}
static final class UserProfile {
private final String displayName;
private final String email;
UserProfile(String displayName, String email) {
this.displayName = displayName;
this.email = email;
}
public String getDisplayName() {
return displayName;
}
public String getEmail() {
return email;
}
}
}
이 예시에서 요청값은 두 개의 비즈니스 키 중 하나만 선택할 수 있습니다. OGNL 엔진에 전달되는 문자열은 항상 코드에 정의된 displayName 또는 email 리터럴입니다.
Apache Struts 심층 방어
다음 Struts 설정은 고정 표현식을 사용하는 설계를 보완하는 심층 방어입니다. 공격자가 선택한 표현식을 안전하게 만드는 무해화 수단은 아닙니다.
- 클래스·패키지 허용 목록인
struts.allowlist.enable을 유지하고 애플리케이션에 필요한 항목만 명시적으로 허용합니다. Struts 7에서는 기본적으로 활성화됩니다. struts.ognl.excludedNodeTypes로 필요하지 않은 AST 노드 유형을 차단하는 OGNL Guard를 구성합니다.- 가능하면
struts.ognl.valueStackFallbackToContext=false를 사용해 ActionContext로의 폴백 접근을 비활성화합니다. struts.ognl.allowStaticFieldAccess=false,struts.disallowProxyObjectAccess=true,struts.disallowDefaultPackageAccess=true,struts.ognl.disallowCustomOgnlMap=true와 같은 Struts 7의 제한 기본값을 완화하지 않습니다.struts.ognl.expressionMaxLength를 애플리케이션에 필요한 최소값으로 유지합니다. 길이 제한은 복잡도를 줄일 뿐 표현식의 의미를 검증하지 않습니다.
Apache Struts 문서에 따르면 -Dognl.security.manager 기반 샌드박스는 JDK 21 이상에서 작동하지 않습니다. 이를 보안 경계로 권장하거나 최신 JDK의 대안으로 제시하지 않습니다.
표현식 사용 경로 점검
사용자 정의 도우미나 템플릿에서 문자열을 나중에 평가하는 경우에도 표현식과 데이터를 분리하세요. 인증·인가에 사용되는 평가 결과와 루트 객체의 공개 범위를 함께 검토하세요.