티스토리 뷰

반응형

개요

챕터 2에서는 CORS를 어떻게 설정하면 보안에 취약해지는가를 알아보기 위해서,

1) 취약한 설정 확인 방법 2) 대응방안을 알아보도록 한다.

 

 

확인 과정

일단 Burp Suite나 ZAP를 통해 패킷을 잡아서

요청 헤더에 Origin이 있으면 조작해보고, 응답 헤더가 어떻게 반환되는지 확인하는 것이

기본적인 확인 방법이다. (앞으로 작성할 취약한 CORS를 확인하는 모든 유형에 대해)

 

 

유형 (1) 설명

CORS 관련 골치아픈 오류를 회피하기 위해,

1) 응답 Access-Control-Allow-Origin 헤더(이하 ACAO 헤더)를 요청 Origin 그대로 응답하도록 설정하고, 

2) Access-Control-Allow-Credentials 헤더(이하 ACAC 헤더)를 True로 응답하도록 개발하는 경우이다.

앞서 살펴보았듯, ACAC 헤더를 True로 설정하는 경우 쿠키를 포함한 크리덴셜 정보를 응답 허용하게 된다.

이 유형의 경우 Credential 정보를 어떤 Cross-origin에서 요청하든 허용하게 되는 격으로,

CORS 정책의 의도를 완전히 무시하는 것이 된다.

또한 이러한 유형은 취약한 CORS 중 가장 흔히 볼 수 있는 케이스라고 한다.

 

 

취약점 확인

실제 취약한 코드 발견

💥 PHP

public funtion example() 
{
    header('Access-Control-Allow-Origin: ' .$_SERVER['HTTP_ORIGIN']);
    header('Access-Control-Allow-Credentials: true');
    ...
}

ACAO 헤더를 php의 서버 변수인 $_SERVER의 HTTP_ORIGIN으로 할당했는데,

이는 PHP로 구성된 웹서버에서 Origin 헤더의 값을 그대로 가져오는 게 된다.

 

💥 Java

public void example(ServletRequest req, ServletResponse res, ...) {
    HttpServletReqeust request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;
    
    String origin = request.getHeader("Origin");
    response.setHeader("Access-Control-Allow-Origin", origin);
    ...
}

이 경우도 위와 동일하다.

 

패킷 확인

Same Origin에서 POST 요청으로 테스트를 해보면, CORS 요청과 동일 취급받는다고 하였다.

Origin 헤더가 추가되고 이에 대한 ACAO, ACAC 헤더를 확인 가능했다.

 

이 Origin을 임의의 url인 red.soya.vul.check로 조작해본다.

조작한 Origin이 그대로 ACAO 헤더에 반영되었다.

만약 Credential 정보가 포함된 요청인 경우, ACAC 헤더만 true가 아니었어도 응답에 접근하지 못했을 수 있다.

현재는 취약 & 취약 조건을 모두 가졌다.

악성 페이지 링크를 통한 공격자의 Origin 공격으로부터 잠재적으로 취약하다고 판단한다 . (자세한 조건은 아래에)

 

CSRF token이 있었다면?

만약 위 케이스에서 요청의 파라미터에 CSRF token이 있거나 CSRF를 방지하는 메커니즘이 있다고 가정하자. 

CORS 정책의 취약만으론 csrf 공격에 한해 보안적으로 취약하다고 보기 어렵다고 생각될 수 있다.

웹서비스 접근을 통해 로그인 후 정상적으로 사용자 hidden form에 있는 식별 값을 같이 보낸다면,

자바스크립트를 통한 Cross-origin에서의 요청은 token값이 없으므로 서버에서 응답을 거부할 수 있기 때문이다.

하지만 드물게 XSS 취약점까지 공존하는 경우, CSRF token 조차 탈취될 가능성이 있어 

CSRF token 등으로 CSRF 공격을 방지하는 경우 + CORS 정책이 안전하게 설정된 경우를 모두 만족해야만 한다.

더불어 근본적으로 토큰이 탈취 불가하도록 XSS 취약점이 먼저 제거되어야 한다고 생각된다.

 

 

대응방안

간단하게 반사된 ACAO의 사용과 ACAC를 무조건 true로 응답하지 않으면 된다.

 

헤더 설정

Access-Control-Allow-Origin 헤더의 경우

응답할 도메인을 화이트리스트로 관리해야 한다.

Access-Control-Allow-Credentials 헤더의 경우

ACAO를 충족하지 않으면 false로 설정하도록 해야 한다.

(기타) ACAO를 와일드카드(*)로 설정하는 경우

보통은 인증정보가 포함되지 않은 서버를 *로 설정하여 CORS 에러를 회피할 수 있는데,

이런 경우 ACAC를 항상 false로 설정해야 한다. 

어차피 ACAO가 *이면 ACAC는 true일 수 없고 에러가 반환되므로 위반할 일은 없을 것 같다. 

 

소스코드 단 조치

위 헤더 설정을 소스코드에서 하는 방법을 주석을 포함해 작성해보았다.

💯 PHP

<?php

// 허용할 Origin들을 화이트리스트로 관리합니다.
$allowedOrigins = array(
    'https://example.com',
    'https://subdomain.example.com'
);

// 현재 요청의 Origin을 가져옵니다.
$requestOrigin = $_SERVER['HTTP_ORIGIN'];

// 현재 요청의 Origin이 화이트리스트에 있는지 확인합니다.
if (in_array($requestOrigin, $allowedOrigins)) {
    // 화이트리스트에 있는 경우 ACAO 헤더를 설정하여 해당 Origin을 허용합니다.
    header('Access-Control-Allow-Origin: ' . $requestOrigin);
    // 보안 인증 정보를 요청에 포함하도록 ACAC 헤더를 설정합니다.
    header('Access-Control-Allow-Credentials: true');
} else {
    // 화이트리스트에 없는 경우 ACAO 헤더를 설정하지 않습니다.
    // 인증 정보를 요청에 포함하지 않으므로 ACAC 헤더를 false로 설정합니다.
    header('Access-Control-Allow-Credentials: false');
}

// 추가적인 CORS 헤더를 설정합니다.
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');

?>

 

💯 Java

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebFilter("/*")
public class CORSFilter implements Filter {

    // 허용할 Origin들을 화이트리스트로 관리합니다.
    private static final String[] allowedOrigins = {
        "https://example.com",
        "https://subdomain.example.com"
    };

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 현재 요청의 Origin을 가져옵니다.
        String requestOrigin = request.getHeader("Origin");

        // 현재 요청의 Origin이 화이트리스트에 있는지 확인합니다.
        if (isAllowedOrigin(requestOrigin)) {
            // 화이트리스트에 있는 경우 ACAO 헤더를 설정하여 해당 Origin을 허용합니다.
            response.setHeader("Access-Control-Allow-Origin", requestOrigin);
            // 보안 인증 정보를 요청에 포함하도록 ACAC 헤더를 설정합니다.
            response.setHeader("Access-Control-Allow-Credentials", "true");
        } else {
            // 화이트리스트에 없는 경우 ACAO 헤더를 설정하지 않습니다.
            // 인증 정보를 요청에 포함하지 않으므로 ACAC 헤더를 false로 설정합니다.
            response.setHeader("Access-Control-Allow-Credentials", "false");
        }

        // 추가적인 CORS 헤더를 설정합니다.
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type");

        chain.doFilter(req, res);
    }

    private boolean isAllowedOrigin(String origin) {
        for (String allowedOrigin : allowedOrigins) {
            if (allowedOrigin.equals(origin)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void destroy() {}
}

 

 

반응형
댓글
반응형
Recent Post.
Recent Reply.
Thanks for comming.
오늘은
명이 방문했어요
어제는
명이 방문했어요