소개
Harbor 회원 포털은 비밀번호를 잊은 사람이 계정을 복구하도록 돕습니다. 재설정 메일의 임시 토큰을 가진 사람은 특정 계정의 비밀번호를 한 번만 바꿀 수 있어야 합니다. 그러나 지원팀은 링크를 재사용하거나 다른 계정의 비밀번호를 바꿀 수 있음을 발견했습니다.
먼저 가상 계정을 정상적으로 복구하고, 위험한 동작을 관찰한 뒤 작은 Python 처리 함수를 고칩니다. Python 경험은 필요 없습니다. 편집 전에 함수와 조건을 설명합니다. 터미널 명령 입력과 탭 전환에는 익숙해야 합니다.
애플리케이션은 Portal Web, 모의 메일은 Mailbox Web, 준비된 코드는 App Server를 사용합니다. 모두 같은 독립 학습 인스턴스에서 실행되며 외부 메일은 보내지 않습니다. 토큰, 만료 시간 비교, 트랜잭션, 비밀번호 변경, 로그인은 실제로 동작하지만 운영용 신원 시스템은 아닙니다.
계정을 정상적으로 복구하기
이 단계에서는 재설정 메일을 따라 비밀번호를 바꾸고 로그인하여 수정 후에도 유지해야 할 정상 동작을 확인합니다.
계정은 사람을 식별하고 비밀번호는 로그인할 수 있음을 증명합니다. 비밀번호를 잊으면 링크가 변경 권한을 일시적으로 부여합니다. 링크 안의 토큰은 비밀번호가 아닙니다. 로컬 사서함에서 가상 계정 maya와 noah의 소유자로 행동할 수 있으며 초기 비밀번호는 모두 StartPass42!입니다. 이 학습 계정만 사용하세요.
Portal Web에서 Request a reset link를 찾습니다. Recovery account에 maya를 입력하고 Send reset link를 누릅니다. Link requested. Open Local Mailbox.가 표시됩니다. Mailbox Web을 새로 고치면 최신 메일이 위에 나타나며 받는 사람, Open reset link, 토큰 문자열이 보입니다.
그 메일의 Open reset link를 클릭합니다. 같은 탭에서 Token과 Reset account가 채워진 양식이 열립니다. 두 필드는 그대로 두고 New password에 FirstPass42!를 입력하여 Reset password를 누릅니다. Reset accepted와 HTTP 200이 예상됩니다.
HTTP는 브라우저와 서버가 요청과 응답을 주고받는 방식입니다. 여기서 200은 승인, 403은 거부, 500은 애플리케이션 오류입니다. 연결 오류와 보안상의 거부는 다릅니다.
Portal Web을 열고 필요하면 새로 고칩니다. Sign in에서 Login account를 maya, Password를 FirstPass42!로 입력하고 Sign in을 누릅니다. Login accepted가 보여야 합니다. 올바른 재설정은 페이지 글자뿐 아니라 실제 비밀번호를 바꿉니다.
링크를 잃으면 같은 양식으로 다시 요청하고 최신 메일을 사용하세요. 유효 시간은 30분입니다. 만료나 오타 때문에 환경을 다시 시작할 필요는 없습니다.
무너진 경계 관찰하기
이 단계에서는 일회용 시연 변경으로 토큰의 소유자, 유효 기간, 한 번의 사용 권한을 구분합니다.
브라우저는 이름과 값이 있는 필드를 보냅니다. 사용자가 수정할 수 있으므로 서버는 메일이 자동 입력했다는 이유만으로 Reset account를 신뢰하면 안 됩니다. 서버에는 별도 토큰 기록이 있습니다.
| 필드 | 의미 |
|---|---|
account |
토큰 사용이 허용된 계정 |
expires_at |
고정된 기준 시점부터 초로 저장한 마지막 허용 시각 |
used |
사용 전 0, 사용 후 1 |
새 토큰은 만료 전에 소유자만 사용할 수 있습니다. 성공하면 사용됨으로 바뀌며 다시 사용 가능해지면 안 됩니다. 미사용 토큰도 시간이 지나면 만료됩니다. 사용됨과 만료됨 모두 추가 재설정을 허용하지 않습니다.
Portal Web에서 maya의 새 링크를 요청합니다. Mailbox Web을 새로 고쳐 최신 메일을 열고 DemoPass42!로 변경합니다. 같은 메일의 링크를 다시 열어 ReplayDemo42!로 변경합니다. 둘 다 Reset accepted입니다. 두 번째는 거부되어야 합니다. 이것이 재전송, 즉 이미 소진되어야 할 권한의 재사용입니다.
같은 링크를 다시 엽니다. Reset account를 noah, New password를 CrossDemo42!로 바꾸고 Reset password를 누르면 승인됩니다. Portal Web에서 noah와 CrossDemo42!로 로그인합니다. maya용 링크가 noah를 바꿨습니다. 서버가 토큰과 계정을 결합하지 않았습니다.
마지막으로 Portal Web 재설정 양식에서 Token을 expired-demo, Reset account를 maya, New password를 ExpiredDemo42!로 입력하고 Reset password를 누릅니다. 이 토큰은 환경 준비 한 시간 전에 만료되었는데도 승인됩니다. 서버가 시간을 무시하므로 타이머를 기다릴 필요가 없습니다.
이 계정은 일회용 시연 대상이며 이후 수정 테스트는 깨끗한 복사본을 사용합니다. 관찰한 상태를 유지하세요. 데이터베이스를 직접 편집할 필요는 없습니다.
토큰을 계정에 결합하고 소모하기
이 단계에서는 작은 서버 함수를 수정하고 실제 시연 계정을 바꾸지 않으며 테스트합니다.
App Server로 전환합니다. 시작 디렉터리는 /home/labex/project/reset입니다. cat은 텍스트 파일을 표시하며 인수는 파일 이름입니다. 처리 함수를 읽습니다.
cat handler.py
첫 줄은 준비된 보조 함수 두 개를 가져옵니다. def는 함수를 정의하고 괄호 안의 이름은 매개변수, 즉 한 번 호출할 때의 입력입니다. eligible(row, account, now)는 저장된 토큰 기록, 제출한 계정, 현재 서버 시각을 받습니다. row["account"]는 기록의 필드를 읽습니다. row is None은 토큰을 찾지 못했다는 뜻입니다. 들여쓴 if 본문은 조건이 참일 때만 실행됩니다. return False는 권한을 거부하고 return True는 부여합니다. 여기서는 앞쪽 공백 네 개가 Python 블록을 만듭니다.
현재 함수는 존재하는 모든 토큰에 True를 반환하므로 세 문제가 발생합니다. reset 역시 토큰을 소모하지 않고 비밀번호만 바꾸는 unsafe_reset을 호출합니다.
수정에는 모든 조건이 함께 필요합니다. ==는 값 비교, >는 큼, and는 양쪽 모두 참을 요구합니다. or는 어느 한쪽만 참이어도 승인하여 정상 속성이 잘못된 속성을 가릴 수 있습니다. 미래 만료 시각은 now보다 커야 하며 같은 시각이면 이미 만료입니다. used == 0은 미사용 토큰만 허용합니다.
consume_and_reset은 SQLite 트랜잭션을 시작하고 현재 토큰을 읽어 여러분의 eligible을 호출한 뒤 비밀번호 변경과 used = 1을 함께 확정합니다. 이것이 원자성, 두 변경이 함께 성공하는 성질입니다. 읽기 전에 잠가 두 경쟁 요청이 모두 미사용 상태를 보는 일을 막습니다. 잠금이나 비밀번호 해시를 직접 구현하지는 않습니다.
터미널 텍스트 편집기 nano로 작은 파일을 엽니다.
nano handler.py
내용을 다음 코드로 교체합니다. 방향키와 Backspace/Delete로 기존 텍스트를 지운 뒤 전체를 붙여 넣을 수 있습니다. 공백과 구두점을 정확히 유지하세요. 괄호가 있어 긴 조건을 여러 줄로 쓸 수 있습니다.
from framework import unsafe_reset, consume_and_reset
def eligible(row, account, now):
if row is None:
return False
return (row["account"] == account
and row["expires_at"] > now
and row["used"] == 0)
def reset(db, token, account, password):
return consume_and_reset(db, token, account, password, eligible)
Ctrl+O로 저장하고 Enter로 handler.py를 확인한 뒤 Ctrl+X로 나옵니다. 각 Web 요청이 저장된 함수를 읽으므로 서비스 재시작은 필요 없습니다. HTTP 500이 보이면 다시 열어 들여쓰기, 괄호, 따옴표를 비교하세요.
python3는 Python 프로그램을 실행합니다. 준비된 검사기는 함수 파일 경로를 받아 임시 SQLite 데이터로 별도 HTTP 앱을 실행합니다. 코드를 고치거나 실제 계정을 바꾸지 않고 실제 함수를 시험합니다.
python3 /opt/reset-lab/check.py handler.py
valid reset accepted, replay denied, competing use has exactly one winner를 포함한 열한 줄의 PASS가 예상됩니다. 안전하지 않은 편집은 FAIL로 실패 속성을 표시합니다. 전부 거부하면 정상 재설정과 로그인 검사에 실패합니다. 브라우저 필드나 실제 DB를 바꿔도 새 데이터에서 시험하는 코드가 고쳐지지는 않습니다. 필요하면 nano handler.py를 다시 열어 고치고 저장한 뒤 같은 검사기를 반복하세요.
복구 기능이 유지됨을 증명하기
이 단계에서는 수정한 실제 서비스를 사용하고 격리 보안 검사와 결과를 연결합니다.
Portal Web에서 maya의 새 링크를 요청하고 Mailbox Web을 새로 고쳐 최신 메일을 연 뒤 ResetSafe42!로 변경합니다. Reset accepted와 HTTP 200이 예상됩니다. 같은 메일의 링크를 다시 열어 maya에 ReplayAgain42!를 시도하면 Reset denied와 HTTP 403이어야 합니다.
다른 새 maya 링크를 요청하고 열어 Reset account를 noah로 바꾼 뒤 BlockedCross42!를 시도합니다. 거부되어야 하며 소유자의 미사용 링크를 소모하면 안 됩니다. 같은 메일을 다시 열어 maya를 유지하고 ResetSafe42!를 설정하면 성공해야 합니다.
양식에서 Token expired-demo, Reset account maya, New password BlockedExpired42!를 시도하면 거부됩니다. 이어 Portal Web에서 maya와 ResetSafe42!로 로그인합니다. Login accepted는 실패한 시도가 비밀번호를 바꾸지 않았음을 보여 줍니다. noah도 시연 비밀번호 CrossDemo42!로 로그인할 수 있습니다. 관련 없는 계정은 유지됩니다.
App Server에서 일회용 데이터 회귀 검사를 반복합니다.
python3 /opt/reset-lab/check.py handler.py
동시 두 번 사용 중 한 번만 성공하는 경우를 포함해 열한 항목이 모두 통과해야 합니다. 반복해도 실제 계정 기록은 바뀌지 않습니다. 정상 복구와 악용 거부를 함께 만족해야 하며 페이지를 끄거나 무조건 거부하는 것은 올바른 수정이 아닙니다.
학습 앱은 배달을 모의하고 신원 처리를 단순화했습니다. 운영 환경에는 안전한 메일, 빈도 제한, 세션 처리, 비밀번호 정책도 필요하지만 이 수정 범위 밖입니다. OWASP 지침이 더 넓은 배경을 제공합니다.
요약
실제 재설정을 수행하고 재사용, 다른 계정 변경, 만료 무시 문제를 시연한 뒤 작은 Python 함수를 고쳤습니다. 저장된 소유자, 미래 만료, 미사용 상태, 원자적 소모가 함께 작동합니다. 일회용 HTTP 검사는 정상 복구, 악용 거부, 다른 계정 유지, 경쟁 시 한 번의 성공을 증명합니다. 직원 포털 챌린지에서 다른 불완전한 조건에 같은 규칙을 적용합니다.



