로그
에루라보

블로그 작업노트 29: 댓글 무차별 대입공격 방어 및 섬네일 추출 방식 변경

이번 긴 연휴 기간동안 블로그를 가끔가다 들어오게 되었는데, 약 2-30건의 무차별 댓글 대입 공격이 있었다는걸 눈치채게 되었다.


댓글 무차별 대입 공격 방어

서버에 남은 로그 기록을 바탕으로 체크해보니 역시 '그 나라' IP여서 아이피 대역 자체를 차단할까 싶었는데, 또 어찌될지 몰라서 일단은 아이피 블록 리스트를 만들어놔서 내가 지정한 아이피는 댓글을 못쓰게 막아두었다.

public static $denyIP = [
    '000.000.000.000'
];

public static function isDeniedIP()
{
    return in_array(Request::ip(), self::$denyIP);
}Copy

위와같은 메소드를 Comment 모델에 구현해두고 댓글을 작성하는 메소드에 아래처럼 조건식을 하나 걸어주면 된다.

if (Comment::isDeniedIP()) {
    return response('해당 IP에서는 댓글을 작성할 수 없습니다.', 403);
}Copy


하지만 이래서는 일이 발생하고 나서 처리하는 방식에 불과했고, 백단에서 비정상적인 댓글 기록을 막기위한 추가 방책이 필요했다.

그래서 최근 5분이내에 3건이상의 댓글을 쓴 유저의 경우에는 일반적인 댓글의 작성시간을 고려해도 너무 빈번하다 생각이 되므로 그런 경우에는 예외처리를 할 수 있도록 기능을 추가해보았다.

위의 Comment 모델에 추가한 메소드처럼 아래 메소드를 추가해주었다.

public static function isTooFrequent(string $ip, int $threshold = 3, int $minutes = 5): bool
{
    return self::where('ip_address', $ip)
            ->where('created_at', '>=', now()->subMinutes($minutes))
            ->count() >= $threshold;
}Copy

그리고 위 메소드를 실행시키는 구문을 댓글 추가 기능에 더하면된다.

public function addComment(Request $request, $id)
{
    if (Comment::isDeniedIP()) {
        return response('해당 IP에서는 댓글을 작성할 수 없습니다.', 403);
    }
    if (Comment::isTooFrequent($request->ip())) {
        return response('너무 잦은 댓글 등록으로 인해 잠시 후 다시 시도해주세요.', 429);
    }

    ....Copy

이러면 쉽게 백단에서 IP 기반으로 빈도가 높은 작업에 대한 예외처리를 실행할 수 있다.
위 작업을 하면서 기존에 에러메시지가 영어로뜨던 부분도 전부 한글로 보일 수 있게 보정작업을 했고, 현재 테스트해본바 정상적으로 예외처리가 작동된다는걸 확인했다.



섬네일 추출 방식 변경

기존에 나는 정규식을 통해서 특정 class 명을 가지고있는 줄을 추출해 그 줄의 src 주소를 가져오는 방식의 코드로 섬네일을 추출했다.

// `class="og_image"`가 포함된 이미지는 제외하고, `https://cdn.erulabo.com`로 시작하는 이미지 src만 추출
preg_match("/<img(?:(?!class=\"og_image\").)*?src=[\"'](https:\/\/cdn\.erulabo\.com[^\"]+)[\"'][^>]*>/i", $post->body, $images);
$post->image = $images[1] ?? null; // 매칭된 이미지 URL이 없으면 nullCopy

근데 이렇게 하니까 좀 문제가 발생했는데, 클래스에 og_image와 더불어 fr-dib, fr-div 이런 클래스명이 더해진다면 해당 정규식에 걸러지지 않는다는것이었다.

간단히 만들 수 있을줄 알았는데, 생각보다 0...* 처리가 정규식상에선 힘들다고 한다.
그래서 해당 기능을 좀 무겁지만 DOM 파서 기능을 통해 아래와 같이 재구현해보았다.

public function extractMainImage(): void
{
    if (!$this->body) {
        $this->image = null;
        return;
    }

    $dom = new \DOMDocument();
    libxml_use_internal_errors(true);
    $dom->loadHTML('<?xml encoding="UTF-8">' . $this->body);
    libxml_clear_errors();

    $imgs = $dom->getElementsByTagName('img');
    foreach ($imgs as $img) {
        $src = $img->getAttribute('src');
        $class = $img->getAttribute('class');
        if (!str_contains($class, 'og_image')) {
            $this->image = $src;
            return;
        }
    }

    $this->image = null;
}Copy

재사용이 가능하게 모델 메소드로 따로 빼서 구현해보았다.
그리고 위 코드를 아래와 같이 이용하면 끝이다.

if ($request->input('use_image')) {
    $post->extractMainImage();
} else {
    $post->image = null;
}Copy

그래서 실제 글의 섬네일을보면,

본문 이미지

이런식으로 보여지는데, 글의 내부를보면 섬네일에 사용된 이미지보다 상위에,

본문 이미지

오픈그래프 링크의 이미지가 있다.
하지만 위 코드의 목적에 맞게 og_image 클래스를 가지고있는 img는 무시하고 아래의 img의 src가 추출됨을 확인할 수 있다.

이로써 가끔가다 이상하게 섬네일이 잡혔던 문제를 해결했다.

#문제해결 #블로그
0 개의 댓글
에루라보 콜렉션의 다른 글
×