[시큐어코딩 가이드] 2-2-16. 반복된 인증시도 제한 기능 부재

반복된 인증시도 제한 기능 부재

정의

일정 시간 내에 여러 번의 인증 시도 시 계정 잠금 또는 추가 인증 방법 등의 충분한 조치가 수행되지 않는 경우, 공격자는 성공할 법한 계정과 패스워드들을 사전으로 만들고 무차별 대입하여 로그인 성공 및 권한 획득이 가능하다.

 

안전한 코딩기법

최대 인증시도 횟수를 적절한 횟수로 제한하고 설정된 인증 실패 횟수를 초과할 경우 계정을 잠금하거나 추가적인 인증 과정을 거쳐서 시스템에 접근이 가능하도록 한다.

 

코드예제

다음은 안전하지 않은 코드예제로, 사용자 로그인 시도에 대한 횟수를 제한하지 않는 코드입니다.

<!--login.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그인</title>
</head>
<body>
    <h2>로그인 페이지</h2>
    <form method="post">
        {% csrf_token %}
        <label>아이디: <input type="text" name="user_id"/></label><br>
        <label>패스워드: <input type="text" name="user_pw"/></label><br>
        <input type="submit" value="로그인">
    </form>
    <p>{{ msg }}</p>
</body>
</html>
#반복된 인증시도 제한 기능 부재(안전X)
def get_user_pw(user_id):
    #비밀번호는 #1234
    return "238cd4ee477fbae8561e1fe82f8e56a7b6e278b501f17efbdb516bcd79c0453d"
def login_bad(request):
    if request.method == 'GET':
        return render(request, 'login.html')

    user_id = request.POST.get('user_id')
    user_pw = request.POST.get('user_pw')

    sha = hashlib.sha256()
    sha.update(user_pw.encode('utf-8'))

    hashed_passwd = get_user_pw(user_id)
    #print(sha.hexdigest()) #get_user_pw 찾기 위해 사용ㅎㅎ

    if sha.hexdigest() == hashed_passwd:
        return render(request, 'success.html', {'msg': '로그인 성공'})
    else:
        return render(request, 'login.html',{'msg': '로그인 실패 다시~'})

 

아이디: apple / 패스워드: 1010

 

아이디: apple / 패스워드: #1234

 

다음은 안전한 코드예제로, 사용자 로그인 시도에 대한 횟수를 제한하여 무차별 공격에 대응하는 코드입니다.

#secure 폴더 안에 models.py 작성
#작성 후 makemigration -> migrate

from django.db import models

# Create your models here.
class Login(models.Model):
    user_id = models.CharField(max_length=10, null=False, blank=False)
    fail_count = models.IntegerField()
#반복된 인증시도 제한 기능 부재(안전O)
LOGIN_TRY_LIMIT = 3
def login_good(request):
    if request.method == 'GET':
        return render(request, 'login.html')

    user_id = request.POST.get('user_id')
    user_pw = request.POST.get('user_pw')

    sha = hashlib.sha256()
    sha.update(user_pw.encode('utf-8'))

    hashed_passwd = get_user_pw(user_id)

    if sha.hexdigest() == hashed_passwd:
        # 로그인 성공 시 실패 횟수 삭제
        Login.objects.filter(user_id=user_id).delete()
        return render(request, 'success.html', {'msg': '로그인 성공'})

    if Login.objects.filter(user_id=user_id).exists():
        login_fail = Login.objects.get(user_id=user_id)
        fail_count = login_fail.fail_count
    else:
        fail_count = 0

    if fail_count >= LOGIN_TRY_LIMIT:
        return render(request, 'error.html', {'msg': 'account_lock'})
    else:
        # 로그인 실패 횟수 DB 기록
        # 첫 시도라면 DB에 insert
        # 실패 기록이 존재한다면 update
        Login.objects.update_or_create(
            user_id=user_id,
            defaults={"fail_count": fail_count+1}
        )

        return render(request,'login.html', {'msg': f'로그인을 다시 시도해주세요. 실패 횟수 : {fail_count+1}'})

 

아이디: apple / 패스워드: 3321

 

3번이상 오류 시

 

아이디: banana/ 패스워드: #1234

 

Login DB 상태

 

 

 

보안기능/반복된 인증시도 제한 기능 부재

[참고문헌] Python 시큐어코딩 가이드(2022) / KISA(한국인터넷진흥원)