본문으로 바로가기
728x90
반응형

목차

     

    과거 프로젝트에서 겪은 문제와 해결했던 방법들을
    기록하고자 이 글을 적어본다

     

    개요 

    신입 백엔드 개발자로 입사해 회사의 첫 프로젝트에서 개발할 당시 경험한 일이다. 이 당시 진행하던 프로젝트는 조합 운영 관리 플랫폼이었고 여타 프로젝트가 그렇듯 User/Admin으로 작업 영역을 나눠둔 상황이었다.

     

    Admin 측에서는 사용자들을 대상으로 특정 문서를 발행해 전달해야 했으며 이 문서는 미리 정의해 놓은 html을 비즈니스 로직에 따라 pdf로 변환했다. Admin은 주로 플랫폼 운영자분들이 사용하는 영역이었기에 회의를 거친 후 pdf 생성에 편의성을 높이기 위해 Jinja2 Template을 도입했다.

     

    3년이 지나 쓰는 글이기에 정확히 Jinaj2 Template 도입이 pdf 생성에 어떤 편의성을 증대시키는지 기능인지 구체적으로 기억은 안 나지만 미리 정의된 변수들을 Jinja2 Template 문법으로 불러들여 pdf를 생성한다는 부분이었던 듯싶다. 

     

     


     

    1.  wkhtmltopdf로  pdf 생성

    미리 정의해놓은 html을 pdf로 변환하기 위해서는 wkhtmltopdf라는 리눅스 바이너리를 사용했었다. 리눅스 바이너리이기 때문에 유추할 수 있듯 서버 내부에 wkhtmltopdf를 설치해 놓고 WAS에서 이 바이너리를 실행할 때 인자값을 넣어 실행하는 방식이었다.

     

    대충 위와 같은 방식이었으며 wkhtmlpdf로 나오는 결과를 Preview 형태로 제공할것인지, 파일로 다운로드할 수 있게 제공할 것인지에 대한 기능이 각각 존재했다.

     

    앞서 언급했듯 Admin에서는 content를 입력하면 API Server에서 html(text)로 wkhtmltopdf에 전달하는 과정에서 html로 변환하기 위해 Jinja2 Template를 사용했다. 

     

    아래 코드는 당시 프로젝트에 사용했던 코드 중 일부다.

    class TemplateRegistPreview(ApiView):
    
        class argument:
            subject = Argument(key='subject', methods=['post'], ge=1)
            content = Argument(key='content', methods=['post'], ge=1)
    
        def post(self):
            """ PDF 미리보기"""
            iatc = TemplateConverter(
                subject=self.argument.subject.value,
                content=self.argument.content.value,
            )
            return HttpResponse(iatc.pdf, content_type='application/pdf')

    subject와 content를 입력받아 생성될 pdf에 대해 Preview를 제공하는 API의 View 계층의 코드다. Response 부분에 "iatc.pdf" 로 wkhtmltopdf에서 생성된 pdf의 내용을 TemplateConver 클래스를 통해 읽어 들이며 이 클래스에는 다음과 메서드들이 정의되어 있다.

    class TemplateConver:
    	...
        
        @property
        def body(self):
            return TemplateParser(self.content).html
    
        @property
        def html(self):
            html = "<document-wrapper>\n"
            html += f"<article><h1>{self.subject}</h1></article>\n"
            html += f"{self.body}\n"
            html += f"{self.footer}\n"
            html += "</document-wrapper>\n"
            return html
    
        @property
        def pdf(self):
            variables = {k: f"<span style='background:#FFFA82'>{k}</span>"
                         for k in undeclared_variables(self.html)}
            with Pdf(
                    pdf_body_html=Template(self.html).render(variables),
                    with_contract_css=True,
                    with_footer_left='preview') as pdf:
                return pdf.content

     


    2. 무엇이 문제였을까?

    당시 나는 TryHackMe나 picoCTF와 같은 플랫폼을 통해 간단한 해킹 문제나 모의 침투 문제를 풀어내는 걸 취미로 삼고 있었다. 자연스레 진행 중인 프로젝트에서 익혀놓은 지식을 써먹어보기 위해 탐구 중이었는데 Admin이 Jinja2 문법을 허용한 입력이 가능하니 위협요소가 발생할 것이라 예측도 할 수 있게 되었다. (지금 생각해 보면 자체 서비스를 만드는 회사였기에 이런 시도도 과감히 할 수 있었던 것 같다.) 

     

    Jinja2는 템플릿 엔진이다. 그러니 발생할 수 있는 위협요소에 흔히 떠올릴 수 있는 기법에 SSTI가 있었다. SSTI는 Server Side Template injection 이라고도 불리는 이 기법은 템플릿 엔진이 사용자가 템플릿 문법의 입력 값을 그대로 해석하여 결괏값을 보여주는 것을 통해 확인이 가능하다.

     

    대표적으로 "{ 7*7 }"을 입력한 경우 49라는 결과를 보여주면 SSTI의 발생한다는 신호다. 진행 중인 프로젝트에서 내가 테스트하려던 관점은 SSTI가 가능하다면 서버는 얼마나 많은 PDF 생성을 감당할 수 있는지에 대한 관점이었다. 즉, Jinja2의 for loop을 이용해 BackEnd Server에 PDF를 대량으로 생성할 수 있는 Jinja2 문법을 입력하는 것이다. 

     

    Jinja2 문법으로 pdf 1만 장을 생성하도록 입력하자 BackEnd Server가 다른 요청을 받지 못하도록 멈춰버렸다.  

     

    나) pdf 생성 시 JinjaTemplate 생성시 SSTI가 가능하기 때문에 Server가 죽는 것 같습니다.

     

     

    이 사실을 있는 그대로 보고했다. 다행히 시키지도 않은 일을 했다고 욕먹진 않았다. 그러나 문제를 발견했으니 해결법을 찾아 제시해야하는게 다소 무거운 부분으로 다가왔다. 

     


    3. Reverse Proxy Issue

    한참 해당 이슈를 재현하던 중에 예기치 못한 문제 하나를 더 발견했다. pdf 1만 장을 생성하는 것은 서버가 처리를 진행하는 중에 멈춰버리는 것이었고 이와는 별개로 pdf 생성수를 2~3천 장 사이로 줄여 테스트한 경우 Reverse Proxy에서 전송 데이터의 크기가 설정된 크기보다 크기에 413 Status Code를 뱉고 있었다.

     

    413은 "Request Too Entity"이다. 이는 설정된 크기보다 더 큰 입력을 요청하면 발생한다. Reverse Proxy로는 Nginx를 사용 중이었는데  Nginx에는 "client_max_body_size"를 보면  default가 1M다. 즉, pdf 1만 장보다는 줄였지만 1M보다 큰 데이터(pdf 생성 html text)를 요청했기 때문에 Nginx에서 이를 처리하지 못하는 상황을 발견했던 것이다.

     

    이는 설정값을 변경함으로써 해결할 수 있었지만 BackEnd에서 PDF 생성을 어떻게 컨트롤할 수 있을지 해결책이 없었다.

     


    4. 문제를 해결 아이디어와 해결법

    BackEnd에서 PDF 생성 관련 기능을 제어하기 위해 가장 먼저 고민했던 방법은 "생성될 PDF 수를 미리 예측할 수 있는지"였다. 입력 텍스트로부터 생성될 PDF를 미리 예측하고 예측된 결괏값을 특정 크기로 제한하는 방법이었는데 wkhtmlpdf에서는 이 기능을 제공하지 않았다.

     

    또 다른 방법으로는 PDF 생성수를 10장으로 제한하는 방법이었다. 하지만 이는 입력 텍스트로부터 PDF를 생성하고 난 후 검증하는 방법이기에 생성 중에 10장이 넘어가는 방법을 체크할 수 없었기에 시도할 수 없었다.

     

    딱히 해결 방법은 떠오르지 않았는데 그간 생성된 PDF 문서의 글자수의 통계를 내고 입력된 데이터가 이 글자수를 기준으로 오차 범위 내에 있으면 생성을 허용하는 방법으로 해결했다.

     

    해결 방법을 도입하는 과정에서 SSTI로 위협을 가할 수 있는 코드도 검증하는 방법을 심어놨다.

     

     


    5. 마치며

    보안 지식이나 해킹기법이 개발을 하면서 언제 써먹지라는 생각이 들 때가 있었다. 잘 만들어진 라이브러리를 잘 가져다 사용한다면 코드상에서 발생할 수 있는 문제들이 없을 것이라 지레짐작하기 때문이었을 것이다. 그러나 가져다 사용하는 것에는 크고 작은 위험이 따르는 것 같다.

     

    한 번씩은 만들어놓은 걸 다른 관점에서 생각해 파고들다 보면 뜻밖의 경험과 경험치를 축적할 수도 있지 않을까.

    728x90
    반응형