사전 서버 사이드 요청 위조
문제

서버 사이드 요청 위조

gabury1고친 사람 github-actions[bot]

서버 사이드 요청 위조는 서버가 바깥 사람이 고른 곳으로 요청을 보내게 되는 문제입니다. 사용자가 건넨 주소를 서버가 대신 불러오는 기능에서 생깁니다. 서버는 바깥에서 닿지 않는 내부 시스템에도 닿습니다. 그래서 막아 둔 안쪽이 서버를 거쳐 바깥에 열립니다.

쉽고 빠른 이해

서버가 남의 부탁을 받고 안쪽 문을 대신 두드리는 문제입니다. 이미지 주소를 넣으면 서버가 그 그림을 받아 와 미리보기를 만드는 기능을 떠올려 봅시다. 그 주소가 사내 관리 화면을 가리키면 서버는 그 화면을 대신 불러옵니다.

곤란한 까닭은 서버가 선 곳에 있습니다. 서버는 방화벽 안쪽에 있습니다. 안쪽 시스템들은 서버가 보낸 요청을 믿고 받습니다. 바깥 사용자는 직접 못 들어가는 곳에 서버의 손을 빌려 닿게 됩니다.

어떻게 터지나:

  1. 사용자가 서버에 주소 하나를 건넵니다
  2. 서버가 그 주소가 어디를 가리키는지 따지지 않고 요청을 보냅니다
  3. 요청이 바깥에서는 막혀 있던 내부 시스템에 닿습니다

막는 데도 대가가 있습니다. 불러올 수 있는 곳을 목록으로 좁히면 아무 주소나 넣던 편리함이 줄어듭니다. 서버에서 나가는 길까지 막으려면 네트워크 설정도 함께 손봐야 합니다.

상세

부탁받은 서류를 가져오는 안내 직원

회사 건물 로비에 안내 직원이 앉아 있습니다. 직원은 출입증이 있어서 사무실 어디든 드나듭니다. 방문객은 출입증이 없어서 로비보다 안으로 들어가지 못합니다.

방문객이 직원에게 부탁합니다. 「안쪽 서류함에서 서류 하나만 가져다주세요.」 직원이 어느 서류함인지 따지지 않고 다녀오면, 방문객은 한 번도 들어가 본 적 없는 방의 서류를 손에 쥡니다.

서버가 안내 직원입니다. 주소를 건네는 사용자가 방문객입니다.

주소를 받아 대신 불러오는 기능

서버 사이드 요청 위조는 영어로 Server-Side Request Forgery 입니다. 줄여서 SSRF 라고 씁니다. 서버 쪽에서 위조된 요청이 나간다는 뜻입니다. 이 문제는 서버가 사용자에게 받은 주소로 요청을 보내는 기능에서 생깁니다.

그 주소는 대개 URL(Uniform Resource Locator)입니다. 서버는 그 URL 로 HTTP(HyperText Transfer Protocol) 요청을 보내 내용을 받아 옵니다.

백엔드에서 흔히 만나는 기능을 아래 표에 모았습니다. 셋 모두 사용자가 고른 주소로 서버가 요청을 보냅니다.

기능 서버가 하는 일
링크 미리보기 게시글에 붙은 링크의 페이지를 받아 와 제목과 그림을 뽑습니다
주소로 파일 올리기 사용자가 준 주소에서 이미지나 문서를 내려받아 저장합니다
웹훅 등록 일이 생길 때마다 사용자가 등록한 주소로 알림 요청을 보냅니다

표의 셋째 줄에 나온 웹훅은 어떤 일이 일어났을 때 미리 등록해 둔 주소로 서버가 알려 주는 방식입니다. 결제가 끝나면 결제 대행사가 쇼핑몰 서버의 주소로 결과를 보내 주는 것이 그 예입니다.

이 기능들은 쓸모가 있어서 생겼습니다. 문제는 기능 자체가 아니라 서버가 받은 주소를 믿는 데서 옵니다.

서버가 서 있는 네트워크 위치

바깥 사용자와 서버는 같은 네트워크에 있지 않습니다. 사용자는 인터넷에 있습니다. 서버는 방화벽 안쪽의 내부망에 있습니다. 방화벽은 네트워크 경계에서 어떤 연결을 들이고 막을지 정하는 장치입니다.

내부망 안에는 바깥에 열지 않은 시스템이 많습니다. 사내 관리 화면, 데이터베이스, 캐시, 사내 API(Application Programming Interface)가 그렇습니다.

이런 시스템은 흔히 내부망에서 온 요청을 믿습니다. 방화벽이 바깥을 막아 주니 내부망에서 온 요청은 동료가 보낸 것이라고 여깁니다. 로그인이나 권한 확인 없이 답하는 내부 시스템이 드물지 않은 까닭입니다.

서버는 내부망 안에 있으므로 이 시스템들에 닿습니다. 사용자가 고른 주소를 서버가 불러오면, 사용자는 서버의 네트워크 위치를 빌려 쓰는 셈이 됩니다.

아래 그림은 사용자에게서 내부 시스템으로 가는 두 길을 보입니다. 바로 가는 길은 방화벽에서 막힙니다. 서버를 거치는 길은 내부 시스템까지 닿습니다.

flowchart TD
    U["인터넷의 사용자"]
    subgraph N["방화벽 안쪽의 내부망"]
        S["서버"]
        A["관리 화면"]
        D["데이터베이스"]
        C["캐시"]
        P["사내 API"]
    end
    U --x|"방화벽이 막는다"| A
    U -->|"주소를 건넨다"| S
    S -->|"서버가 대신 요청한다"| A
    S --> D
    S --> C
    S --> P

클라우드의 메타데이터 서비스

클라우드의 가상 머신에는 그 머신 안에서만 부를 수 있는 메타데이터 서비스가 딸려 있습니다. 머신에서 도는 프로그램이 자기 설정을 물어보는 창구입니다.

자격 증명은 「나는 이 자원을 써도 되는 주체다」를 보여 주는 값입니다. 비밀번호나 접근 키가 여기에 듭니다. 메타데이터 서비스는 클라우드 자원을 부를 때 쓰는 임시 자격 증명도 내줍니다.

이 창구는 머신 밖에서는 안 닿습니다. 하지만 그 머신에서 도는 서버가 요청을 대신 보내면 닿습니다. 클라우드에서 이 문제가 나면 피해가 머신 하나에서 그치지 않는 까닭입니다. 그 자격 증명이 닿는 클라우드 자원까지 번질 수 있습니다.

아래 그림은 피해가 머신 밖으로 번지는 길을 위에서 아래로 보입니다.

flowchart TD
    U["인터넷의 사용자"]
    subgraph VM["가상 머신"]
        S["서버"]
        M["메타데이터 서비스"]
    end
    U --x|"머신 밖에서는 안 닿는다"| M
    U -->|"주소를 건넨다"| S
    S -->|"서버가 대신 요청한다"| M
    M -->|"내준다"| K["임시 자격 증명"]
    K -->|"이 값으로 부른다"| R["클라우드 자원"]

한 요청에 섞인 두 주인

이 문제의 뿌리는 한 요청 안에서 권한을 가진 쪽과 지시를 내린 쪽이 갈라지는 데 있습니다. 요청을 보내는 주체는 서버입니다. 네트워크는 서버의 권한을 보고 요청을 받습니다. 그런데 어디로 보낼지는 바깥 사용자가 정합니다.

권한을 가진 쪽이 권한 없는 쪽의 지시를 대신 수행하게 되는 이 구조를 혼동된 대리인 문제라고 부릅니다. 영어로는 confused deputy 라고 합니다. 로비의 안내 직원이 이 대리인입니다.

터지는 조건

SSRF 는 세 조건이 겹칠 때 문제가 됩니다. 하나라도 빠지면 서버가 바깥에서 받은 주소로 요청을 보내도 피해로 이어지지 않습니다. 아래 그림은 조건을 하나씩 거르며 내려갑니다.

flowchart TD
    A["사용자가 준 값이<br/>요청의 목적지를 정하나"] -->|아니오| X["문제가 안 된다"]
    A -->|예| B["서버가 목적지를<br/>따지지 않고 보내나"]
    B -->|아니오| X
    B -->|예| C["그 서버에서<br/>바깥이 못 닿는 곳에 닿나"]
    C -->|아니오| X
    C -->|예| D["서버 사이드 요청 위조가 된다"]

첫째 조건은 설계에서 끊습니다. 사용자가 주소 전체를 적지 않고 미리 정한 목록에서 하나를 고르게 하면, 목적지는 서버가 정합니다.

둘째 조건은 애플리케이션 코드에서 끊습니다. 요청을 보내기 전에 목적지가 허용된 곳인지 확인하는 검사가 이 조건을 맡습니다.

셋째 조건은 네트워크 설정에서 끊습니다. 바깥 주소를 불러오는 서버가 애초에 내부망으로 요청을 못 보내게 해 두면, 앞의 검사가 새더라도 내부 시스템에 닿지 않습니다.

응답이 안 돌아와도 생기는 피해

서버가 불러온 내용을 사용자에게 보여 주지 않는 기능도 있습니다. 웹훅처럼 요청만 보내고 끝나는 기능이 그렇습니다. 이런 경우를 따로 블라인드 SSRF 라 합니다.

응답을 안 보여 줘도 요청은 목적지에 닿습니다. 받은 요청만으로 상태를 바꾸는 내부 시스템이면 그것으로 피해가 납니다. 요청이 성공했는지 실패했는지만 달라져도 내부망에 무엇이 있는지가 드러납니다. 응답을 숨기는 것만으로는 막지 못합니다.

목적지를 서버가 정하는 설계

막는 법의 첫째는 사용자에게 주소를 받지 않는 것입니다. 사용자는 주소 대신 식별자를 고릅니다. 식별자에 맞는 주소는 서버가 들고 있습니다. 연동할 외부 서비스를 목록에서 고르게 하는 식입니다. 그 서비스의 주소는 서버 설정에 둡니다.

주소를 꼭 받아야 하는 기능이면 허용 목록으로 좁힙니다. 허용 목록은 가도 되는 곳만 적어 두고 나머지를 전부 거절하는 방식입니다. 반대로 가면 안 되는 곳만 적어 두는 방식은 차단 목록입니다.

이 문제에서는 차단 목록이 잘 샙니다. 같은 목적지를 가리키는 표기가 여럿이라 빠짐없이 적기 어렵습니다. 같은 기계를 localhost 라는 이름으로도, 127.0.0.1 이라는 숫자로도 부를 수 있는 것이 그 예입니다. 허용 목록은 적지 않은 곳이 전부 막히므로, 빠뜨려도 막히는 쪽으로 틀립니다.

허용 목록에는 호스트 이름과 함께 스킴도 적습니다. 스킴은 URL 맨 앞의 https 같은 부분입니다. 어떤 방식으로 연결할지를 정합니다. 웹 페이지만 불러오면 되는 기능이면 https 하나만 허용합니다.

검사한 주소로 연결하기

주소 검사는 순서가 중요합니다. URL 에 적힌 것은 대개 도메인 이름입니다. 연결은 그 이름을 조회해 얻은 IP 주소(Internet Protocol address)로 갑니다.

도메인 이름을 IP 주소로 바꾸는 조회는 DNS(Domain Name System)가 맡습니다. 같은 이름이라도 조회할 때마다 다른 IP 주소가 돌아올 수 있습니다.

이름의 조회 결과는 그 도메인을 가진 쪽이 정합니다. 주소를 건넨 바깥 사용자가 자기 도메인을 적었다면, 조회 결과도 그 사용자가 정합니다.

그래서 이름만 보고 검사한 뒤 연결할 때 다시 조회하면, 두 번의 조회 결과가 서로 다른 곳을 가리킬 수 있습니다. 이 틈을 노리는 기법을 DNS 리바인딩이라고 부릅니다. 아래 그림은 이 틈을 서버 쪽에서 봅니다.

sequenceDiagram
    participant 서버
    participant DNS
    participant 내부 as 내부 시스템
    서버->>DNS: 이름을 조회한다
    DNS-->>서버: 인터넷의 IP 주소
    Note over 서버: 검사 통과
    서버->>DNS: 연결하려고 다시 조회한다
    DNS-->>서버: 내부망의 IP 주소
    서버->>내부: 연결한다

막는 법은 이름을 한 번만 조회하는 것입니다. 그 결과로 검사하고 같은 결과로 연결합니다.

검사할 때 걸러 낼 곳은 내부망을 가리키는 주소입니다. 대표적인 것을 아래 표에 모았습니다.

거를 주소 무엇을 가리키나
사설 IP 주소 내부망 안에서만 쓰려고 떼어 둔 주소 범위
루프백 주소 요청을 보낸 기계 자신
메타데이터 서비스의 주소 앞에서 본 가상 머신의 설정 창구

셋 모두 바깥 사용자가 직접은 못 닿는 곳입니다. 조회한 IP 주소가 이 중 하나면 연결하지 않고 거절합니다.

리다이렉트는 「그 주소 말고 이 주소로 가라」고 돌려보내는 응답입니다. 검사를 통과한 주소도 이 응답으로 다른 곳을 가리킬 수 있습니다.

아래 그림은 지금까지의 순서를 지키는 요청 한 번을 위에서 아래로 보입니다. 거절되지 않고 끝까지 내려와야 연결이 나갑니다.

flowchart TD
    A["사용자에게 받은 URL"] --> B["스킴과 호스트 이름이<br/>허용 목록에 있나"]
    B -->|아니오| X["거절"]
    B -->|예| C["이름을 한 번 조회해<br/>IP 주소를 얻는다"]
    C --> D["그 IP 주소가<br/>내부망을 가리키나"]
    D -->|예| X
    D -->|아니오| E["그 IP 주소로 연결한다"]
    E -->|리다이렉트 응답| B

서버 코드에서 HTTP 요청을 보내는 라이브러리를 HTTP 클라이언트라고 합니다. 이 라이브러리가 리다이렉트를 저절로 따라가게 되어 있으면, 검사를 통과한 요청이 검사 안 한 곳으로 옮겨 갑니다.

막으려면 리다이렉트를 자동으로 따라가지 않게 끕니다. 따라가야 하는 기능이면 옮겨 갈 때마다 처음부터 같은 검사를 거칩니다. 그림에서 마지막 화살표가 맨 위의 검사로 되돌아가는 까닭입니다.

네트워크에서 한 번 더 막기

애플리케이션의 검사는 코드 한 곳이 틀리면 뚫립니다. 네트워크에서 같은 일을 한 번 더 막는 까닭입니다. 이렇게 겹겹이 막는 원칙이 심층 방어입니다.

바깥 주소를 불러오는 일은 전용 서비스 하나로 모읍니다. 그 서비스에는 인터넷으로 나가는 연결만 허용합니다. 내부망으로는 못 나가게 방화벽 규칙을 겁니다. 서버에서 나가는 연결을 거르는 이런 규칙이 아웃바운드 규칙입니다.

아래 그림은 앞의 네트워크 그림에 이 규칙을 더한 모습입니다. 다른 서비스는 바깥을 직접 부르지 않고 전용 서비스에 맡깁니다.

flowchart TD
    subgraph N["방화벽 안쪽의 내부망"]
        S1["서비스 A"]
        S2["서비스 B"]
        F["바깥을 불러오는 전용 서비스"]
        I["내부 시스템<br/>관리 화면 · 데이터베이스 · 캐시 · 사내 API"]
    end
    S1 --> F
    S2 --> F
    F -->|"허용"| W["인터넷"]
    F --x|"아웃바운드 규칙이 막는다"| I

모아 두면 지켜볼 곳도 하나로 줄어듭니다. 여러 서비스가 저마다 바깥을 부르면 서비스마다 검사와 규칙을 따로 챙겨야 합니다.

내부 시스템이 스스로 지키기

내부 시스템 쪽도 손봅니다. 내부망에서 왔다는 이유만으로 요청을 믿지 않습니다. 내부 호출에도 인증을 겁니다. 네트워크 위치로 신뢰를 주지 않는 이 원칙을 제로 트러스트라고 합니다.

클라우드에서는 머신에 붙는 권한을 꼭 필요한 만큼만 줍니다. 최소 권한 원칙입니다. 메타데이터 서비스에서 자격 증명이 새더라도 그 자격 증명으로 할 수 있는 일이 좁아집니다.

불러온 응답 다루기

서버가 불러온 내용을 받은 모양 그대로 사용자에게 돌려주지 않습니다. 기능에 필요한 것만 뽑아 돌려줍니다. 이미지를 가져오는 기능이면 받은 바이트를 이미지로 읽어 새로 만듭니다. 이미지가 아니면 버립니다.

이렇게 하면 요청이 엉뚱한 곳에 닿았더라도 그곳의 내용이 사용자에게 흘러가지 않습니다. 오류 메시지도 같은 이유로 뭉뚱그립니다. 내부에서 무슨 오류가 났는지 자세히 알려 주면 내부망 사정이 드러납니다.

응답 크기와 기다리는 시간에도 상한을 둡니다. 끝없이 이어지는 응답이나 답이 없는 목적지에 서버 자원이 묶이지 않게 하려는 것입니다. 기다리는 시간의 상한이 타임아웃입니다.

코드와 운영에서 찾아내기

찾는 일은 코드 리뷰에서 시작합니다. 사용자 입력이 HTTP 클라이언트의 주소 인자까지 흘러가는 길을 따라갑니다.

입력이 어디서 들어와 어디까지 닿는지 따라가는 방식을 오염 분석이라고 합니다. 바깥에서 들어온 값에 「오염됐다」는 표시를 붙입니다. 그 표시가 위험한 곳까지 닿는지를 봅니다. 코드를 실행하지 않고 읽어서 이 일을 해 주는 정적 분석 도구도 있습니다.

설계 단계에서는 위협 모델링 때 「서버가 바깥으로 요청을 보내는 기능」을 따로 목록으로 뽑습니다. 목록에 오른 기능마다 목적지를 누가 정하는지 적어 둡니다. 사용자가 정한다고 적힌 기능이 점검 대상입니다.

운영에서는 나가는 연결의 로그를 봅니다. 인터넷 주소만 부르던 서버가 내부망 주소로 요청을 보내면 경보가 울리게 합니다. 바깥을 부르는 일을 전용 서비스로 모아 두었다면 그 서비스의 로그 하나만 보면 됩니다.

바깥을 부르는 기능이 있는 서비스라면 이 문제는 보안 점검표와 침투 테스트 범위에 흔히 들어갑니다. 침투 테스트는 허락을 받고 시스템을 공격자의 눈으로 점검해 보는 일입니다. 점검하는 쪽은 앞에서 본 세 조건이 기능마다 끊겨 있는지를 확인합니다.

이 이름으로 부르지 않는 것

크로스 사이트 요청 위조는 이름이 비슷하지만 속는 쪽이 다릅니다. 그쪽에서는 사용자의 브라우저가 속아 사용자의 쿠키를 단 요청을 보냅니다. 서버 사이드 요청 위조에서는 서버가 속아 서버의 네트워크 위치에서 요청을 보냅니다. 둘 다 요청을 보내는 쪽과 그 요청을 시킨 쪽이 다릅니다.

오픈 리다이렉트는 서버가 사용자를 남이 고른 주소로 돌려보내는 문제입니다. 다음 요청을 보내는 쪽은 사용자의 브라우저라서 서버의 네트워크 위치를 빌리지 않습니다.

경로 조작은 사용자가 준 값이 파일 경로를 정해, 서버가 열면 안 되는 파일을 여는 문제입니다. 구조는 같습니다. 대상이 네트워크 대신 파일 시스템입니다.

XML 외부 엔티티 문제는 XML(eXtensible Markup Language) 문서를 읽을 때 생깁니다. XML 은 태그로 데이터를 적는 텍스트 포맷입니다.

XML 문서에는 다른 곳의 내용을 끌어오라고 적어 둔 줄을 넣을 수 있습니다. 이런 외부 참조를 외부 엔티티라고 합니다.

문서를 읽어 프로그램이 다룰 구조로 바꾸는 부품은 파서입니다. 파서는 외부 참조를 따라 요청을 보낼 수 있습니다. 그 요청이 내부망으로 가면 결과가 SSRF 와 같아집니다. XML 을 받는 서버가 파서의 외부 참조 불러오기를 꺼 두는 까닭입니다.

다섯을 같은 칸으로 나란히 놓으면 아래와 같습니다.

이름 속는 쪽 빌리는 것 대상
서버 사이드 요청 위조 서버 서버의 네트워크 위치 내부망의 시스템
크로스 사이트 요청 위조 사용자의 브라우저 사용자의 쿠키 사용자가 로그인한 사이트
오픈 리다이렉트 사용자의 브라우저 믿을 만한 사이트의 주소 남이 고른 주소
경로 조작 서버 서버의 파일 접근 권한 파일 시스템
XML 외부 엔티티 서버의 파서 서버의 파일·네트워크 접근 파일 시스템 · 내부망

속는 쪽과 빌리는 것을 같이 봐야 갈립니다. 서버가 속아 네트워크 위치를 빌려 주는 것이 서버 사이드 요청 위조입니다. XML 외부 엔티티는 파서를 거쳐 같은 결과에 이를 수 있습니다.

관련 항목

서버 사이드 요청 위조가 속하는 분류

취약점 · 웹 보안 · OWASP · 보안 엔지니어링 · 공격 표면

서버 사이드 요청 위조가 생기는 서버 기능

웹훅 · 링크 미리보기 · URL · HTTP 클라이언트 · 리다이렉트 · HTTP

서버 사이드 요청 위조로 닿는 내부 자원

내부망 · 인스턴스 메타데이터 서비스 · 자격증명 · 사설 IP 주소 · 루프백 · 데이터베이스 · 캐시 · API

서버 사이드 요청 위조가 비롯된 권한 구조

혼동된 대리인 문제 · 앰비언트 권한 · 신뢰 경계 · 권한 상승

서버 사이드 요청 위조를 막는 설계 원칙

허용 목록 · 차단 목록 · 입력 검증 · 심층 방어 · 최소 권한 · 제로 트러스트

서버 사이드 요청 위조를 막는 네트워크 설정

방화벽 · 아웃바운드 규칙 · 네트워크 분리 · 프록시 · 타임아웃

서버 사이드 요청 위조 검사가 기대는 주소 해석

DNS · 도메인 이름 · IP 주소 · DNS 리바인딩 · URL 파싱

서버 사이드 요청 위조를 찾는 활동

코드 리뷰 · 오염 분석 · 정적 분석 · 위협 모델링 · 침투 테스트 · 로그

서버 사이드 요청 위조와 헷갈리는 이웃 문제

크로스 사이트 요청 위조 · 오픈 리다이렉트 · 경로 조작 · XML 외부 엔티티 · 교차 사이트 스크립팅 · SQL 인젝션

다른 이름: SSRF · Server-Side Request Forgery · 서버 측 요청 위조 · 서버사이드 요청 위조