payment-lab 기술적 이슈 -3- 결제 이력 및 복구, Logger를 그대로 사용해도 되는걸까?
이 게시글은 payment-lab이라고 하는 결제모듈 연동 사이드프로젝트를 진행하던 중 발생한 기술적 이슈를 해결하기 위해 정리하는 글입니다.
스프링 프레임워크에는 로깅을 직관적으로 사용하도록 도와주는 Log 관련 라이브러리들이 있습니다. 구체적으로는 log4j, logback 등을 사용하지만 스프링에서는 ‘slf4j’ 로그 통합 인터페이스를 제공하는 라이브러리를 통해 로깅을 합니다. payment-lab 에서는 로그를 단순히 애플리케이션의 동작 이력 및 디버깅 참고용으로 기록하는 것을 넘어서, 결제 정산 및 백업 그리고 복구를 수행하는데 활용됩니다.
그래서 처음에는 logback 설정을 통해 별도의 로거를 생성하여 결제 이력을 기록했습니다.
logback-spring.xml
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<!-- 기존 애플리케이션 로거 설정 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application-%d{yyyy.MM.dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 결제 이력 로거 설정 -->
<appender name="PAYMENT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/payment.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/payment-%d{yyyy.MM.dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<!-- 애플리케이션 및 결제 로거 사용 -->
<logger name="org.springframework" level="INFO"/>
<logger name="payment" level="INFO" additivity="false">
<appender-ref ref="PAYMENT_FILE" />
</logger>
</configuration>
결제 승인 요청 및 이력 로깅
@Component
class TossPaymentsProcessor(
....
): PaymentsProcessor {
// 결제 이력 전용 로거
private val logger = LoggerFactory.getLogger("payment")
@Transactional
override fun keyInPay(
paymentOrderId: Long,
amount: Int,
//....
customerIdentityNumber: String
) {
....
// 결제 승인 요청
val response = tossPaymentsKeyInApprovalProcessor.approval(
paymentOrder,
TossPaymentsKeyInDto(
.....
)
)
// 결제 승인 결과 db 저장
val newPaymentRecord = tossPaymentsRepository.save(newPaymentEntity)
newPaymentRecord.pollAllEvents().forEach {
// 결제 승인 결과 로깅
logger.info("complete {}", objectMapper.writeValueAsString(it))
publisher.publishEvent(it)
}
}
}
위의 코드를 베이스로 결제 승인이 무사히 완료되면, 아래와 같은 포맷으로 로그가 기록됩니다.
22:02:35.238 [http-nio-8080-exec-5] INFO payment - complete {"accountId":1,"approvedAt":"2023-08-07T13:02:35.000+00:00","orderId":"ord_202308072202349775950217","orderName":"정상 결제 이벤트 발행4","amount":10000,"paymentKey":"evl2J9MNzjkYG57Eba3G6wMxgMebN68pWDOxmA1QXRyZ4gLw","status":"DONE","occurredOn":"2023-08-07T13:02:35.225+00:00"}
그럼 이제 추후에 해당 로그들을 통해, 정산 작업 이전에 서버의 db와 pg의 결제이력과 크로스체크를 수행할 때, 해당 로그에서 json 포맷으로 입력된 문자열을 활용하면 될 것 같습니다.
하지만 이러한 방식은 아래와 같은 문제점들을 가지고 있습니다.
1. 해당 기능은 단일 책임 원칙을 위반합니다.
스프링의 Logger는 오직 logging 만을 위해 사용해야 합니다. 따라서, 다른 동료가 해당 logger를 제가 의도한 방향과 다르게 결제 이력 말고도 다른 결제와 관련된 정보를 로깅할 여지가 있습니다.
이렇게 용도와 다른 로그가 production 서버에 로깅이 되는 순간 기존 정산 로그와 내용이 섞일 수 있는데, 이러한 문제는 파이프라인에 버블을 발생시키기 때문에 운영에 어려움이 생길 수 있고, 추후 유지보수 작업에 병목이 일어날 수도 있습니다.
2. 보안 취약점이 발생할 수 있습니다.
지금은 해결이 되었지만, 한 때 log4j 혹은 logback을 통해 다양한 기능을 수행하는 과정에서 발생된 다양한 취약점 문제가 발견되어 화재가 되었던 적이 있습니다.
취약점이 발생하게 된 이유는 라이브러리 자체의 취약점도 있지만, 개발자들이 로그를 로깅하는 것 외에도 low 레벨단에서 코드를 원격으로 실행하는 작업과 같이 로그 이외의 작업들을 수행하려 했던 것들이 문제였습니다.
같은 맥락으로 결제 이력의 세부 내용들을 로거 출력에 string으로 그대로 출력한다면, 해당 로거 출력 내용이 탈취당했을 때, 악용될 여지가 있습니다.
그럼 한때 어떤 취약점이 있었고 무슨 위험성이 있었는지 확인해보겠습니다.
보안 취약점의 대표적인 사례:CVE-2021-42550
https://cve.report/CVE-2021-42550
내용을 설명하기에 앞서, 위의 링크에 나온 내용을 잠깐 살펴보겠습니다
Certain versions of Cloud Manager from Netapp contain the following vulnerability:
In logback version 1.2.7 and prior versions, an attacker with the required privileges to edit configurations files could craft a malicious configuration allowing to execute arbitrary code loaded from LDAP servers.
사례 1: Microsoft Exchange Server 대규모 공격
21년 1월, 중국의 해킹그룹 HAFNIUM이 MS Exchange 서버에 접근하여 이메일 계정에 접근하고 백도어를 설치하여 피해자 환경에 대한 액세스를 유지한 사례가 있었다고 합니다.
이 사례는 CVE-2021-26855, CVE-2021-26857, CVE-2021-26858에 해당하는 사례로 위에서 소개한 사례와는 다르지만, 이러한 사례를 통해 logback의 설정 파일을 변조하여 서버의 코드를 가져와 사용자 인증정보를 가져올 수 있는 백도어를 만들 가능성이 있다고 유추해 볼 수 있습니다.
사례 2: Colonial Pipeline
2021년 5월, 미국의 앨라배마 주 펠햄에 있는 Colonial Pipeline 시설이 사이버 공격을 받았습니다. 조사 결과, Darkside 랜섬웨어 조직의 공격으로 밝혀졌으며, 이로 인해 시스템이 폐쇄된 사례가 있습니다. Colonial Pipeline은 암호화된 파일을 복구하기 위하여, 공격자에게 약 5백만 달러 상당의 암호화폐를 지불하고 복호화 키를 얻었습니다.
이러한 사례를 통해 logback의 취약점을 활용하여 백도어를 활용한 랜섬웨어 공격이 가능할 수 있다는 점을 유추해 볼 수 있습니다.
2021년 최악의 사이버 공격사건 정리
CNA 금융 2021년 3월, 미국의 최대 보험사 중 하나인 CNA 파이낸셜이 랜섬웨어의 공격을 받았습니다. 이후 파일에 대한 액세스 권한을 복구하기 위하여 4만 달러를 지불하였습니다. 해커는 Hades 랜섬
blog.alyac.co.kr
자 그럼 이제 제가 이 2 가지의 이슈를 해결하기 위해 각각 어떤 대응을 취했는지에 대해 이야기해 보겠습니다.
1. Custom logger : logger를 직접 사용하기보다, 빈으로 Wrapping
새로운 Logger를 만들었다는 뜻처럼 보일 수 있으나, 전혀 아닙니다. 기존에는 결제 승인을 수행하는 코드에 직접 payment logger를 사용했으나, 앞서 이야기한 것처럼, 다른 동료가 payment logger를 의도와 전혀 다르게 사용할 여지가 있습니다. logger 자체만 있으면 아무 데나 써도 되는 것처럼 보일 수 있으니까요.
그래서 payment-lab의 경우, 결제 트랜잭션 기록 시, logger를 직접 사용하지 않고, 객체(빈)로 감 쌓았습니다.
open class AsyncAppenderPaymentTransactionLogProcessor(
private val objectMapper: ObjectMapper
) {
private val logger = LoggerFactory.getLogger("payment")
@Async
open fun process(event: PaymentResultEvent) {
logger.info(objectMapper.writeValueAsString(event))
}
@Async
open fun process(event: PaymentOrderRecordEvent) {
logger.info(objectMapper.writeValueAsString(event))
}
}
이렇게 만들어진 CustomLogger는 아래와 같이 이벤트를 발행하고 해당 내역을 기록할 때 활용됩니다.
class TossPaymentsTransactionEventPublisher(
private val stringKafkaTemplateWrapper: StringKafkaTemplateWrapper,
private val asyncLogProcessor: AsyncAppenderPaymentTransactionLogProcessor,
//....
) {
fun publishAndRecord(result: TossPaymentsApprovalResponse, paymentOrder: PaymentOrder) {
val currentAccount = getCurrentAccount()
val newPaymentEntity = TossPaymentsFactory.create(result)
newPaymentEntity.resultOf(currentAccount.id, paymentOrder.status)
val newPaymentRecord = tossPaymentsRepository.save(newPaymentEntity)
newPaymentRecord.pollAllEvents().forEach {
asyncLogProcessor.process(it as PaymentResultEvent)
val eventWithClassType =
DomainEventTypeParser.parseSimpleName(objectMapper.writeValueAsString(it), it::class.java)
stringKafkaTemplateWrapper.send(paymentProperties.paymentTransactionTopicName, eventWithClassType)
}
}
}
이렇게 customLogger를 빈으로 활용하면, 결제 승인 결과를 어떻게 기록할지 예측할 수 있습니다. 위의 경우 'AsyncLogProcessor'라고 명명하고 실제로 비동기 스레드로 로깅 처리를 하였는데, 이 부분은 제가 payment-lab에 카프카를 적용한 이유에서 유추해 볼 수 있습니다. 해당 게시글을 보고 싶으시면 아래 링크를 봐주세요.
카프카를 선택한 이유: 높은 내구성과 고가용성 결함에 강한 로그 시스템 갖추기
개요 현재 Payment-lab에서 가장 큰 고민은 결제 승인 데이터의 손실을 최소화하는 것입니다. payment-lab에서는 사용자의 결제 승인 데이터의 정합성을 최대한 맞추기 위해, 무조건 PG api의 결제 승인
postwithmemory.tistory.com
2. Logback 취약점 대응: 읽기 전용으로, 그리고 재 로딩 자체가 불가능하게 하여 변조될 가능성을 없애자
당시 이 취약점이 발견되었을 때 수많은 it 기업이 긴급대응을 하느라, 수많은 개발자들이 밤을 지새웠습니다. 제가 근무했던 보안 회사의 경우, 아예 별도의 logger를 만들어, 해당 취약점이 발생하는 경우를 없앴습니다.
그러나 다행히 23년 8월 기준으로 더 이상 해당 이슈로 보안문제가 발생하는 것 같지는 않습니다. spring은 logback의 문제를 어떻게 해결했을까요?
https://logback.qos.ch/news.html
Removed Groovy configuration support. As logging is so pervasive and configuration with Groovy is probably too powerful, this feature is unlikely to be reinstated for security reasons.
We note that the aforementioned vulnerability requires write access to logback's configuration file as a prerequisite. Please understand that log4 Shell/CVE-2021-44228 and CVE-2021-42550 are of different severity levels. A successful RCE attack with CVE-2021-42550 requires all of the following conditions to be met:
- write access to logback.xml
- use of versions < 1.2.9
- reloading of poisoned configuration data, which implies application restart or scan="true" set prior to attack
위 링크의 내용은 logback이 새 버전을 배포하면서, 개선한 내용들을 구체적으로 작성하였습니다. 많은 내용들이 있었지만, 현 상황에서 가장 참고가 될 만한 문구만 따로 인용하였습니다. 위 내용은 설정과 관련된 유틸성 기능들을 아예 제거하였다고 합니다. 따라서, 개발자는 logback을 사용한다면 아래 세 가지만 조심하면 됩니다.
- logback.xml에 쓰기 권한이 있으면 안 된다.
- 라이브러리 버전은 1.2.9 이상이어야 한다.
- scan="true" 옵션으로 인해, 변조된 설정 파일이 다시 로딩되어 애플리케이션에 적용되어선 안된다.
결론
이번에 결제 이력 로깅을 위해, logback의 설정을 따로 커스터마이징을 수행하였습니다. 이미 라이브러리 제공자 측에서 해결된 버전의 라이브러리를 제공해 주었기 때문에 지금은 로깅 관련해서 취약점이 추가로 발표가 나지 않고 있지만, 로그 라이브러리 관련한 취약점 문제의 종류가 생각보다 다양하여 별도로 조사를 수행하였습니다.
현재 상황에서는 기존 logback 설정을 그대로 사용하는 것이 아니라, 별도의 설정을 구성하였기 때문에 수많은 로깅 취약점 이슈 중에서 CVE-2021-42550, 즉, 설정파일 변조를 통한 민감정보 탈취의 경우에 대비하는 것이 타당하고 보았습니다.
다행히, 이미 logback 라이브러리 제공자 측에서 이 문제를 인지하여 해당 취약점이 개선된 버전을 제공하였습니다. 따라서, 개발자는 제공받은 이상의 버전을 사용하고, 설정파일을 읽기 전용으로 유지하면 적어도 해당 취약점에 대해서는 크게 영향을 받을 것 같진 않습니다.