블로그 운영

블로그 작업노트 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 개의 댓글
블로그 운영 콜렉션의 다른 글
×