
해지 지속성 탐지 실습: 비밀번호 재설정은 성공했지만 공격자가 떠나지 않을 때. Strapi CVE-2026-22706 조건부 해지 버그와 그 수정, 세 가지 규칙 탐지 팩, 그리고 이를 놓치는 순진한 규칙을 재현합니다.
비밀번호 재설정은 성공했지만 공격자가 떠나지 않을 때.
하나의 취약점 클래스를 위한 로컬 레드/블루 랩: 그것을 죽였어야 했던 이벤트보다 더 오래 살아남는 자격 증명. 익스플로잇, 근본 원인, 수정, 세 가지 규칙 탐지 팩, 규칙이 구조적으로 볼 수 없는 것을 위한 상태 감사, 포렌식 콘솔 — 그리고 작동하지 않는 탐지 규칙까지 함께 제공되며, 실패하는 모습을 보여주기 위해 저장소에 보관됩니다.
빠른 시작 · 발견 사항 · 순진한 탐지가 실패하는 이유 · 규칙 팩 · 콘솔 · 수정 · 매트릭스 · 테스트 · 문서
동일한 공격. 동일한 요청. 단 하나의 차이.
콘솔이 그리는 것과 동일한 페이로드로 scripts/figures.py가 생성 — CI에서 재생성 및 diff되므로 그림이 코드와 어긋날 수 없습니다.
09:00 alice logs in ┐ refresh credential rt-001 │ the attacker steals rt-001 ┘
10:00 alice changes her password ← the one thing a victim can do alone HTTP 200 · password changed · fresh session issued
10:00:03 attacker: POST /refresh rt-002 → HTTP 200 new access credential at-004, issued 10:00:03
10:01:00 attacker: GET /me at-004 → HTTP 200 {"username": "alice", "authenticated": true}
오류 없음. 이상 징후 없음. 집계할 인증 실패 없음. 공격자의 접근 자격 증명은 *3초 전에 생성되었으며*, 리셋 이후 요청에 따라 서버가 발급한 것이다.
다음이 취약점의 전부이다:```python
def _revoke_for_security_change(self, user, kind, device_id):
if device_id:
revoke_credentials(user, device_id=device_id) # ← the finding
리뷰어의 시각으로 읽어보자. 폐기 로직은 바로 거기 있다. 올바른 스코프로 올바른 함수를 호출한다. 그 주변의 엔드포인트는 비밀번호 해시를 갱신하고, 200을 반환하며, 새 세션을 발급한다 — 올바른 비밀번호 변경의 모든 관찰 가능한 동작이 존재한다.
그리고 호출자가 device_id를 생략하면, 아무것도 폐기되지 않으며, 엔드포인트는 여전히 성공을 보고한다.
이것은 가상의 시나리오가 아니다. Strapi ≤ 5.33.2의
CVE-2026-22706으로,
리프레시 토큰 무효화 단계가 호출자가 제공한 deviceId에 조건부로 실행되었다.
2.1, Low로 평가되었다.
아래에서 논의한다.
점수가 낮은 이유는 공격자가 이미 접근 권한을 가지고 있었기 때문이다 — 그것이 진입 조건이며, 이 버그가 새로운 것을 부여하지는 않는다. 이 버그가 부여하는 것은 지속 시간이며, 피해자가 스스로 조작할 수 있는 유일한 통제 수단을 무력화함으로써 그렇게 한다.
test_persistence_lasts_as_long_as_the_refresh_credential은 7일의 시뮬레이션 시간을 걸어 이를 보여준다.억제 경로에서의 Low 점수 버그는 기능 경로에서의 Low 점수 버그보다 더 많은 비용을 초래한다. 그 비용은 사고 대응 중에 지불되며, 그때는 아무도 권고문을 읽지 않기 때문이다.
가장 먼저 작성하게 되는 규칙:```text IF credential.issued_at < credential_change.timestamp: ALERT
그것은 어리석은 규칙이 아니다. 비용이 저렴하고, 필드 하나만 필요하며, 문제의 정의처럼 읽히고, **단순한 경우에 대해서는 정확하다** — 재설정 이후 탈취한 *액세스* 토큰을 사용하는 공격자는 잡힌다.
`test_the_naive_rule_catches_the_simple_case`는 이것이 작동함을 검증한다.
두 규칙 모두 동일한 자격 증명에 대해 같은 질문을 던진다. 답을 얻기 위해 서로 다른 필드를 읽으며, 두 필드는 서로 모순된다:

**3초 후, 아니면 한 시간 전 — 같은 순간의 같은 자격 증명.** 순진한 규칙은 공격자가 제어하는 필드를 묻고, `POST /refresh` 한 번이면 그것이 재설정된다.
이 쿼리가 실제로 작성되는 곳인 요청 로그에 대해 실행해 보라. 요청 로그야말로 당신이 가진 것이기 때문이다:```text
NAIVE DETECTOR (NAIVE-001), over the request log
Result: NO ALERT
Six requests were served to the attacker after the reset. Every one of them
carried at-004, minted at 10:00:03 -- three seconds *after* the password
change. By its own timestamp it is the newest credential on the account.
거기서 멈추는 것은 쉬울 뿐만 아니라 부정직한 일이므로, 이 랩은 가장 공정한 버전의 순진한 규칙도 실행합니다 — refresh 엔드포인트까지 감시하도록 확장한 버전입니다:```text NAIVE DETECTOR, widened to include POST /refresh
1 alert at 2026-09-11T10:00:03.000Z: the credential presented to /refresh was rt-002, issued 2026-09-11T09:30:00.000Z. Then it goes blind. 6 events follow that hop and it flags none of them, because every credential from there on carries a post-reset timestamp. Its incident covers 1 accepted request; the lineage rule's covers all of them.
And look at what the alert names: NAIVE-001 revoke rt-002 -- rotated away and already dead at 10:00:03 AFTERLIFE-001 revoke lin-001 -- the live thing every future credential descends from
3시에 중요한 차이는 바로 이것이다. 순진한 규칙은 체인이 경계를 넘는 단 한 순간을 포착한 뒤 남은 29일 동안 흔적을 놓친다 — 그리고 그것이 지목하는 자격 증명은 *이미 서버에 의해 회전되고 폐기된 상태*다. 그것을 폐기하는 것은 아무것도 달성하지 못한다. `lin-001`이 바로 네가 죽여야 하는 객체다.
**그리고 순진한 규칙은 텔레메트리가 부족했던 것이 아니다.** 그것은 동일한 이벤트 스트림, 동일한 계보 인덱스, 동일한 허용 오차, 동일한 중복 제거, 그리고 동일한 경계 상태를 사용한다. 그것은 단 하나의 메서드를 재정의한다:```python
class Correlator: # AFTERLIFE-001
def _age_reference(self, facts):
return facts.root_issued_at
class NaiveCorrelator(Correlator): # NAIVE-001
def _age_reference(self, facts):
return facts.issued_at
root_issued_at은 이미 참조 중인 인덱스에 들어 있습니다.
test_the_naive_rule_had_the_data_it_needed가 이를 증명합니다. 실패는
비교에 있지, 로깅에 있지 않습니다.
하나의 로그 위에 세 가지 규칙. 각각 다른 질문에 답하며, 사고가 실제로 전개되는 순서대로 발동합니다.
10:00:00 HIGH AFTERLIFE-003 Incomplete revocation at a security change 10:00:03 HIGH AFTERLIFE-001 Post-revocation credential lineage use
**그 순서가 이 저장소에서 가장 유용한 부분이다.** AFTERLIFE-003은 변경이 적용되는 순간, 공격자가 무엇이든 건드리기 3초 전에 발동한다. 왜냐하면 그것이 필요로 하는 증거는 이미 완전하기 때문이다. 로그는 어떤 계보가 진입 시점에 살아 있었는지 말해주며, 그것들이 폐기되었다고는 말하지 않는다.
피해자도, 익스플로잇도 필요하지 않다. 그것은 사용자가 수행하는 첫 번째 비밀번호 재설정에서 결함을 보고할 것이다 — 이는 기다릴 공격자가 없는 스테이징에서 실행할 수 있는 유일한 규칙이 된다. AFTERLIFE-001은 침해가 진행 중임을 알려주고, AFTERLIFE-003은 당신의 격리 통제가 깨졌음을 알려준다.

`application recorded watermark: no`는 사고 대응자를 증상이 아니라 *결함*으로 향하게 하는 필드이다.
### 규칙이 사용하는 워터마크는 애플리케이션이 보고하는 것과 다르다
자격 증명 변경 이벤트는 `revocation_watermark` 필드를 운반한다 — 애플리케이션이 기록한 `credentials_valid_after` 값이다. **취약한 구현에서는 이것이 `null`인데, 애플리케이션이 결코 값을 기록하지 않았기 때문이다.** 그것이 바로 버그이다.
따라서 그 필드를 기준으로 삼는 규칙은 그것이 잡아내기 위해 존재하는 바로 그 사례에 대해 눈이 먼 상태가 될 것이다. 규칙은 이벤트 자체의 `timestamp`에 의존하며, 이는 애플리케이션이 제 역할을 했든 안 했든 참이고, 누락된 필드를 증거로 보고한다.
### 속성```text
deduplication 6 accepted requests on the stale lineage -> 1 alert
event order shuffled stream -> same alert (1 alert)
duplicate telemetry log replayed twice -> 1 alert (24 duplicate events discarded)
false positives the fixed implementation's log -> 0 alerts
bounded state caps at 256 activity/user, 2000 users, 20000 credentials
순서 독립성은 "대체로 작동함"이 아니다:
test_6b_every_permutation_of_the_critical_events_detects는 중요한 네 이벤트의
24가지 순서를 모두 실행하고 각각에서 정확히 하나의 경고를 요구한다.
중복 제거는 (user_id, credential_change_event_id, lineage_id)를 키로 하며,
그 앞에 event_id 억제를 두어 재생된 파일이 카운트를 부풀릴 수 없게 한다.
전체 규칙 카드, 필수 텔레메트리 및 대응 런북: docs/detection.md.
이 랩이 다루는 단 하나의 질문을 위한 열람 화면. 경고 카운트 대시보드가 아니라 사망 등록부다. 자격 증명마다 하나의 막대가 발급부터 사망까지 이어지고, 그것이 유래한 계보로 묶이며, 비밀번호 변경은 그 이전의 모든 것이 끝났어야 했던 선으로 그려진다.
의도적인 반전 하나: 따뜻함은 살아 있음을 뜻하며, 그 선 이후의 따뜻함은
잘못된 것이다. 대부분의 보안 UI에서 빨강은 오류가 발생했음을 뜻한다. 여기서는
아무것도 오류가 나지 않는다 — 취약 실행의 모든 요청이 200을 반환한다. 그래서
색은 사망을 따른다: 차가움은 시키는 대로 죽은 자격 증명, 따뜻함은 아직 숨
쉬는 자격 증명이며, 기요틴 너머에서 여전히 따뜻하다는 것이 바로 이 발견의
전부다. (BLACKOUT도 같은 관례를 세웠는데, 거기서는 200이 빨강이었다.)
가느다란 수직 방울은 계보다: 그 순간 부모로부터 발급된 자식 자격 증명.
rt-001 → rt-002 → rt-004는 창 전체에 걸쳐 오른쪽 아래로 내려가며, 취약
모드에서는 절단 이후에도 계속 내려간다 — 이것이 자기 멸종 사건의 반대편에서
새 자격 증명을 발급하는 계보의 그림이다.```bash
python scripts/lab.py console
**[`docs/console-preview.html`](https://github.com/het-p301204/afterlife/blob/main/docs/console-preview.html)**을 작성합니다 — 두 실행이 모두 내장된
자체 포함형 74 KiB 파일입니다. 서버도, 네트워크도, 가져올 폰트도 필요 없습니다. 파일 시스템에서 바로 열면 됩니다. 또는 라이브 버전을 실행하세요:```bash
python -m console
10:00:00 이전으로 스크러버를 되돌렸다가 다시 앞으로 이동시켜 보라: 로그가 그 발견을 정당화하는 시점에 발견 항목이 나타난다. 변경 시점에 AFTERLIFE-003, 3초 후에 AFTERLIFE-001이 나타난다. 무엇이 잘려나갔는지를 포함한 설계 근거는 docs/console-design.md에 있다.
콘솔은 구조적으로 읽기 전용이다. 완료된 시나리오를 다시 실행하고 그려낼 뿐이며, 구현을 바꾸거나 시계를 이동시키거나 무언가를 취소할 수 없다.
test_the_console_has_no_control_surface는 유일한 비-GET 라우트가/api/rebuild임을 검증한다. 읽고 있는 대상을 변경할 수 있는 읽기 표면은 신뢰할 수 없다.
6개의 패키지, 하나의 보안 클래스, 인프라 없음.```text
app/ the lab application
config.py mode selection; defaults to fixed, deliberately
store.py SQLite credential state + the security-change audit trail
tokens.py minting and decoding; a JWT is a signed pointer to a row
auth.py login / refresh / change-password + THE BUG + the audit
main.py six endpoints
common/ clock.py a rewindable UTC clock, so an hour of history costs nothing events.py the event vocabulary, shared by app and detector telemetry.py JSONL emission with credential-name redaction
detector/ strictly downstream: reads a log file, decides nothing base.py the alert shape, replay suppression, bounded state lineage.py rebuilds a credential's ancestry from issuance events ledger.py which lineages are alive, per user rules.py AFTERLIFE-001 persistence reuse.py AFTERLIFE-002 theft containment.py AFTERLIFE-003 the defect itself naive.py NAIVE-001, kept in order to be demonstrated failing audit.py the state scan the rules structurally cannot do engine.py the pack: one shared index, one stream, ranked alerts tail.py byte-offset JSONL tailer
console/ the mortality register payload.py one run, shaped for drawing static/ ~1400 lines of vanilla HTML/CSS/JS, no build step
scripts/ lab.py the demonstration scenarios.py nine scenarios x both implementations report.py the incident report preview.py bake the offline console figures.py render the console to SVG for this README
tests/ 290 tests
애플리케이션은 데모, 콘솔, 테스트 아래에서 **인프로세스**로 실행된다. 포트도, 개발 서버도, Docker도 없다. 시계가 고정되어 있어 모든 실행이 결정적이며, 커밋된 텔레메트리, 콘솔, 그림, 보고서는 실행 간에 모두 바이트 단위로 동일하다 — CI는 이를 `git diff --exit-code`로 검사한다.
### 서버 측 강제
자격 증명은 문자열이 아니라 **행**이다. 클라이언트가 보유한 JWT는 `cid` — 조회 키 — 를 담고 있으며, 그 행이 자격 증명의 유효 여부를 결정한다.
만료는 보유자가 제시한 `exp` 클레임이 아니라 행의 `expires_at`을 기준으로 검사된다. 두 시간 클레임 모두 `verify=False`로 PyJWT에 전달되는데, 여기서 이는 *"서버가 스스로 검사한다"*는 뜻이며, [`app/auth.py`](https://github.com/het-p301204/afterlife/blob/main/app/auth.py)가 모든 요청에서 그렇게 한다. 클라이언트는 자신의 자격 증명이 여전히 유효한지에 대해 발언권을 갖지 못한다.
---
## 공격 타임라인```mermaid
flowchart TD
A["Account compromised<br/><i>phishing · XSS · stolen backup</i>"] --> B["Attacker holds refresh credential rt-001<br/>lineage lin-001, root sess-001 @ 09:00"]
B --> C["Legitimate password change @ 10:00<br/>HTTP 200 · password hash updated"]
C --> D{"device_id supplied?"}
D -->|"yes"| E["revoke_credentials(user, device_id)<br/>rt-001 revoked"]
D -->|"no — the exploit"| F["nothing revoked<br/>no watermark written"]
E --> G["Attacker refresh → 401<br/><b>contained</b>"]
F --> AF3["<b>AFTERLIFE-003 · HIGH</b> @ 10:00:00<br/>containment did not run<br/><i>no attacker action required</i>"]
F --> H["POST /refresh rt-002 → 200<br/>mints at-004 @ 10:00:03"]
H --> I["GET /me at-004 → 200<br/>well-formed · correctly signed · <b>no anomaly</b>"]
I --> J["at-004.issued_at > watermark<br/>NAIVE-001: no alert"]
I --> K["lineage lin-001 root @ 09:00 < watermark<br/><b>AFTERLIFE-001 · HIGH</b>"]
F --> L["attacker holds and never spends<br/>AFTERLIFE-001 silent — correctly<br/><b>state audit: dormant survivor</b>"]
style F fill:#7f1d1d,color:#fff
style H fill:#7f1d1d,color:#fff
style I fill:#7f1d1d,color:#fff
style J fill:#78350f,color:#fff
style K fill:#14532d,color:#fff
style AF3 fill:#14532d,color:#fff
style L fill:#1e3a5f,color:#fff
style E fill:#14532d,color:#fff
style G fill:#14532d,color:#fff
중간에 있는 refresh-chain 분기가 핵심이다. 공격자는 오래된 자격 증명을 사용하지 않는다. 오래된 혈통을 사용하며, 그 혈통은 요청 시 새로운 무언가를 발행한다.
세션은 혈통의 루트이다. 그 아래에서 발행되는 모든 것은 그 루트를 영원히 상속받는다.```text sess-001 session lineage lin-001 root sess-001 issued 09:00:00 │ ├── rt-001 refresh parent sess-001 root_issued_at 09:00:00 │ └── at-001 access parent rt-001 root_issued_at 09:00:00 │ ├── rt-002 refresh parent rt-001 root_issued_at 09:00:00 ← 09:30 rotation │ └── at-002 access parent rt-002 root_issued_at 09:00:00 │ └── rt-004 refresh parent rt-002 root_issued_at 09:00:00 ← 10:00:03 rotation └── at-004 access parent rt-004 root_issued_at 09:00:00 issued_at 10:00:03
`at-004`는 3초 되었다. 그 계보는 한 시간 되었다. 두 사실 모두 참이며,
그중 요청 로그에 보이는 것은 하나뿐이다.
두 가지 불변 조건이 이를 뒷받침하며, 둘 다 테스트된다:
1. **오직 인증만이 계보를 생성한다.** `Store.open_session`은 `lineage_id`가
생성되는 유일한 장소이며, 세션을 자신의 `root_credential_id`로 만든다.
2. **발급은 루트를 아래로 복사한다.** `TokenService.mint`는 계보 필드를
재계산하지 않고 부모로부터 읽어오므로, 자격 증명은 자신이 유래한 로그인보다
더 새로운 계보를 획득할 수 없다.
홀더가 제시한 토큰은 의도적으로 자신의 계보 루트를 **지니지 않는다**. 만약
지닌다면, 홀더가 그것에 대해 거짓말할 수 있기 때문이다.
---
## 상태 감사
AFTERLIFE-001은 오래된 계보가 *사용될* 때 발동한다. 이는 탐지 규칙에게 올바른
트리거이며, 하나의 구멍을 남긴다: **아무도 건드리지 않은 오래된 계보는 그것에게
보이지 않는다.** 자격 증명을 훔치고, 리셋이 실패하는 것을 지켜본 뒤, 기다리는
공격자는 상관 분석할 활동을 만들어내지 않는다.
그래서 이 랩은 규칙이 물을 수 없는 질문도 던진다: *무슨 일이 일어났는가*가
아니라, *무엇이 아직 살아 있는가*.```text
scenario: dormant-survivor (the attacker holds the credential and never spends it)
AFTERLIFE-001 silent — correct; nothing was accepted
AFTERLIFE-003 HIGH — the change revoked nothing
state audit 1 stale lineage, 1 dormant
이 프로젝트에 기여하고 싶으시다면, 다음 방법들을 고려해 보세요:
기여하기 전에, 기여 가이드라인을 읽어주세요.
이 프로젝트는 MIT License에 따라 라이선스가 부여됩니다. 자세한 내용은 LICENSE 파일을 참조하세요.
해피 해킹! 🚀```bash python scripts/lab.py audit
## 주요 기능
- **다중 소스 수집**: Shodan, Censys, FOFA, Hunter, Quake, ZoomEye, Netlas, Criminal IP, PublicWWW, Google, Bing, Baidu, Yandex, 360, GitHub, Gitee, URLScan, VirusTotal, BinaryEdge, FullHunt, Driftnet, Onyphe, WhoXY, HunterHow, ThreatBook, AlienVault, IntelX, BGP, RapidDNS, RSEcloud, 0.zone, BeVigil, Crt.sh, CertSpotter, Chaos, DayDayMap, DNSDumpster, DoH, H1, Hurricane, IPv4Info, JSubFinder, LeakIX, OSV, Recon.dev, Robtex, SpyOnWeb, Sublist3r, TSV, ThreatMiner, Twitter, Wayback, Yahoo, ZoneTransfer, ZoomEye, 天眼查, 爱企查, ICP, 站长之家, 备案查询, 工信部, 全国企业信用信息公示系统, 小蓝本, 企查查, 企业预警通, 天眼查, 爱企查, 启信宝, 水滴信用, 企业信用, 国家企业信用信息公示系统, 中国裁判文书网, 中国执行信息公开网, 信用中国, 中国庭审公开网, 人民法院公告网, 全国法院被执行人信息查询, 全国法院失信被执行人名单信息公布与查询, 中国市场监管行政处罚文书网, 国家知识产权局, 中国版权保护中心, 商标局, 专利局, 中国证券监督管理委员会, 上海证券交易所, 深圳证券交易所, 北京证券交易所, 全国中小企业股份转让系统, 中国货币网, 中国债券信息网, 中国外汇交易中心, 中国金融期货交易所, 上海期货交易所, 大连商品交易所, 郑州商品交易所, 中国期货业协会, 中国证券业协会, 中国上市公司协会, 中国证券投资基金业协会, 中国银行间市场交易商协会, 中国保险行业协会, 中国信托业协会, 中国财务公司协会, 中国融资担保业协会, 中国小额贷款公司协会, 中国典当行业协会, 中国拍卖行业协会, 中国期货市场监控中心, 中国证券登记结算有限责任公司, 中国金融电子化公司, 中国印钞造币总公司, 中国金币总公司, 中国长城资产管理公司, 中国信达资产管理公司, 中国华融资产管理公司, 中国东方资产管理公司, 中国银河金融控股有限责任公司, 中国建银投资有限责任公司, 中国投资有限责任公司, 中央汇金投资有限责任公司, 中国证券金融股份有限公司, 中证指数有限公司, 上海证券交易所信息网络有限公司, 深圳证券信息有限公司, 中证信息技术服务有限责任公司, 中国期货市场监控中心有限责任公司, 中国证券登记结算有限责任公司上海分公司, 中国证券登记结算有限责任公司深圳分公司, 中国证券登记结算有限责任公司北京分公司, 中国证券登记结算有限责任公司上海分公司, 中国证券登记结算有限责任公司深圳分公司, 中国证券登记结算有限责任公司北京分公司```text
VULNERABLE
server state 1 lineage(s) outlived the change (0 dormant)
telemetry 1 lineage(s) -- the same question, asked of the log instead of the database
lin-001 root 2026-09-11T09:00:00.000Z 7 credentials in use
FIXED
server state clean
telemetry clean
두 개의 소스, 의도적으로: 텔레메트리 감사는 로그가 입증할 수 있는 것을 보고, 서버 상태 감사는 데이터베이스가 믿는 것을 본다. 둘이 불일치할 때, 로그는 자격 증명 상태의 충실한 기록이 아니며, 그 위에 구축된 모든 탐지는 보이는 것보다 약하다 — 그래서 명령은 둘 다 출력하고 불일치하면 그렇게 말한다.
서버 상태 감사에는 security_changes 테이블이 필요하며, 이 테이블은 두 모드 모두
기록한다. 변경이 발생했음을 기록하는 것은 그것에 대해 조치를 취하는 것과는 별개의
의무이기 때문이다 — 그리고 취약한 구현은 둘 중 정확히 하나만 충족한다.
두 구현에 대해 아홉 개의 시나리오. python scripts/scenarios.py는
자체적으로 두 개의 불변식을 검증하고 둘 중 하나라도 깨지면 비영(非零)으로 종료한다.
네 개의 행이 논점을 담고 있다:
legitimate-only**에는 공격자가 전혀 없다. 수정본은 아무것도 생성하지 않으며,
취약한 구현은 LOW를 생성한다. 이는 오탐이 아니다 — 이것은
enumeration_only이다: 이번에는 폐기가 작동했다, 열거를 통해, 서버가 잊어버린
자격 증명을 커버할 워터마크 없이. 이 팩은 공격자가 없는 상태에서 두 구현을
구분한다.dormant-survivor**는 사각지대이자 그 답을 한 행에 담고 있다:
AFTERLIFE-001 침묵, AFTERLIFE-003 HIGH, 감사 dormant: 1.multi-device**는 취약한 구현의 작동하는 분기가 여전히 실패하는 곳이다 —
폐기가 노트북에만 적용되고, 다른 두 기기는 손대지 않았다.expired-lineage**는 오탐 통제다: 자격 증명이 스스로 만료된 40일 된 세션은
살아남은 지속 경로가 아니며, 그렇게 보고되지도 않는다.그리고 매트릭스의 핵심 주장은, 스크립트와
test_the_fix_never_produces_a_control_failure_finding에 의해 검증된다: 수정본은
어떤 시나리오에서도 통제 실패 발견을 제기하지 않는다. AFTERLIFE-002는 통과가
허용된다 — 그것은 통제 실패가 아니라 탈취를 보고하며, 올바른 구현에도 여전히
보고할 탈취가 있다.
모든 비밀번호 재설정마다 호출하는 규칙은 일주일 안에 음소거되고, 음소거된 규칙은 규칙이 없는 것보다 나쁘다 — 그것은 모두가 실행 중이라고 믿는 규칙이다.
모든 행에는
tests/test_detector_afterlife001.py와
tests/test_detector_rulepack.py에 테스트가 있다.
2초 허용 오차에 관하여. 이 랩의 모든 타임스탬프는 하나의 프로세스와 하나의
클록에서 나오므로, 정직한 허용 오차는 0이다. 2초는 NTP 하의 현실적인 두 호스트
배포에 필요한 것이며, 이 공격이 만들어내는 간격보다 세 자릿수 더 작다 — 리프레시
자격 증명의 존재 목적 전체가 장수명이라는 것이다. 허용 오차는 진정으로 stale한
계보가 무시되는 창이므로, 의도적으로 작게 유지된다:
test_12b_a_gap_beyond_the_tolerance_does_alert는 2.1초가 여전히 발동함을
고정한다.
self.store.set_watermark(user.user_id, moment) # the guarantee revoked = self.store.revoke_credentials(user.user_id, ...) # defence in depth
`device_id`에 의존하지 않습니다. 이는 컨텍스트로 기록될 뿐이며 블라스트 반경에 영향을 미치지 않습니다.
**워터마크는 아키텍처적 보장입니다.** 사용자당 하나의 타임스탬프인 `credentials_valid_after`는 모든 요청에서 자격 증명 자체의 발급 시점*과* 그 계보 루트 모두와 비교됩니다:```python
watermark = user.credentials_valid_after
if credential.issued_at < watermark: reject # the obvious case
if credential.root_issued_at < watermark: reject # the refresh chain
두 번째 비교는 제대로 구현하려면 비용이 드는 것이며, 순진한 구현이 빠뜨리는 것이다 — 탐지에서와 마찬가지로 강제에서도 마찬가지다.
명시적 폐기는 심층 방어이자 증거다. revoked_at과 revocation_reason은 사고 대응자가 읽는 것이고, auth.token.revoked 이벤트는 격리가 실제로 일어났음을 증명하는 것이다.
test_the_watermark_alone_rejects_a_stale_credential은 아무것도 폐기하지 않은 채 워터마크만 설정하고 오래된 자격 증명이 거부되기를 요구한다 — 이는 둘 중 어느 쪽이 핵심인지, 그리고 서버가 발급했음을 잊어버린 자격 증명에 대해서도 여전히 성립하는 속성이 어느 것인지를 확립한다.
바뀐 요청은 단 하나도 없다. 바뀐 함수는 하나다.
방아쇠는 *"비밀번호가 변경되었다"*가 아니다. *"이전에 발급된 자격 증명을 신뢰할 수 없게 만드는 무언가가 변경되었다"*이다. 네 가지 경로 모두 _revoke_for_security_change를 공유하므로, 수정과 버그가 다음 모두에 동일하게 적용된다:
| 변경 | 자격 증명이 신뢰할 수 없게 되는 이유 |
|---|
모든 종류에 대해, 두 모드 모두에서 tests/test_privilege_changes.py에서 테스트된다 — 로그인이 요구 사항이 아니라는 점까지 포함하여, 다음 독자가 이를 "고치지" 않도록 문서화되어 있다.
자격 증명이 작동을 멈추게 하는 네 가지 방법. AFTERLIFE는 A + C를 구현하며, 순서가 중요하다.
D에 대하여: JWT는 본질적으로 안전하지 않은 것이 아니다. 긴장은 더 좁고 정확히 진술할 가치가 있다 — 무상태 검증과 즉각적인 서버 측 폐기는 상호 배타적이다. "이 자격 증명은 더 이상 유효하지 않다"를 그것을 아는 무언가를 참조하지 않고 결정할 수 없으며, 참조하는 것이 시스템을 상태 기반으로 만든다. 실수는 JWT를 무상태성 때문에 채택하고 나서 어쨌든 폐기가 필요해지는 것이다. 이는 모든 제품이 노트북을 처음 도난당했을 때 겪는 일이다. 결국 얻게 되는 것은 서버 측 상태를 가리키는 서명된 포인터다 — 이는 app/tokens.py가 의도적으로 구현하는 것이며, 의도적으로 거기에 도달하는 것이 사고 도중에 거기에 도달하는 것보다 저렴하기 때문이다.
CVE-2026-22706은 2.1, Low로 평가된다 (CVSS v4.0 AV:N/AC:H/AT:N/PR:H/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N), 그리고 이 점수는 방어 가능하다: PR:H는 공격자가 이미 유효한 리프레시 자격 증명을 보유해야 하기 때문이고, AC:H는 그것을 얻으려면 사전 침해가 필요하기 때문이며, VC:N/VI:L은 이 결함이 공격자가 이미 가지고 있지 않은 접근 권한을 부여하지 않기 때문이다. CVSS는 취약점의 한계적 영향을 측정하며, 한계적 영향은 접근의 지속 시간이지 그 범위가 아니다. 그러한 입력이 주어지면, 2.1은 공식에서 올바르게 도출된다.
CVSS가 모델링하지 않는 것은 실패하는 통제가 격리 조치라는 점이다 — 따라서 유용한 결론은 숫자가 아니라 트리아지 라우팅에 관한 것이다. 격리 경로에서의 Low는 기능 경로에서의 Low가 받지 못할 주의를 받을 가치가 있다. 왜냐하면 그 비용은 사고 중에 지불되기 때문이다. 전체 논의: docs/tradeoffs.md.
290 passed
| file | what it pins |
|---|---|
| [`test_fixed_mode.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_fixed_mode.py) | **절대 실패해서는 안 되는 회귀 테스트 스위트** — 폐기 이벤트 이전에 발급된 자격 증명은, 오래된 계보에서 파생된 것을 포함하여, 그 이후에도 허용되지 않는다 |
| [`test_vulnerable_mode.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_vulnerable_mode.py) | 취약점이 **존재**하고, 결정적이며, 30일 동안 지속되고, 조건문에 의해 발생함을 검증 — `device_id` 제어를 통해 폐기 경로가 죽은 것이 아니라 선택적임을 입증 |
| [`test_lineage.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_lineage.py) | 갱신 체인은 하나의 계보와 하나의 루트를 유지하며, 오직 로그인만이 계보를 생성한다 |
| [`test_detector_afterlife001.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_detector_afterlife001.py) | 7가지 탐지 사례, 9가지 오탐 사례, 2가지 사각지대, 24가지 모든 이벤트 순서, 유계 상태, 잘못된 형식의 입력 |
| [`test_detector_rulepack.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_detector_rulepack.py) | AFTERLIFE-002 및 AFTERLIFE-003 — 모든 판정, 유예 기간, 회전은 죽음이 아님, 만료 제외, 그리고 엔진의 순서 |
| [`test_naive_detector.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_naive_detector.py) | NAIVE-001이 **이 README가 주장하는 바로 그 방식으로** 계속 실패함 — 필요한 데이터를 가지고 있었다는 점을 포함하여 |
| [`test_audit_and_console.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_audit_and_console.py) | 어떤 규칙도 볼 수 없는 휴면 생존자; 두 감사 소스가 일치함; 콘솔이 자체적으로 어떤 판정도 계산하지 않음; 오프라인 미리보기가 아무것도 가져오지 않음; 이 README의 모든 수치가 범위 내에 있고, 스타일시트 없이 바이트 단위로 안정적임 |
| [`test_scenarios.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_scenarios.py) | 논거를 담고 있는 매트릭스 행 |
| [`test_privilege_changes.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_privilege_changes.py) | MFA, 역할 변경 및 계정 복구, 두 모드 모두 |
| [`test_telemetry.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_telemetry.py) | 마스킹 계약, 그리고 발급된 리터럴 bearer 문자열에 대한 로그 grep |
| [`test_app.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_app.py) | 위조된 서명, 잘못된 자격 증명 유형, 서버 상태로부터의 만료 |
| [`test_infrastructure.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_infrastructure.py) | 설정, 시계, 테일러의 부분 라인, 두 CLI, 모든 데모 명령 |
스위트에서 가장 강력한 테스트는
`test_no_bearer_string_or_password_ever_reaches_the_log`이다: 전체 공격을 실행한
다음 로그 파일에서 애플리케이션이 발급한 실제 자격 증명, 두 비밀번호, 그리고
서명 키를 검색한다. 필드 이름이 아니라 — 값에 대해서.
---
## 사각지대
숨기면 랩이 부정직해지므로 분명히 밝힌다. 각각에는 테스트가 있다.
> **애플리케이션이 자격 증명 변경 이벤트를 내보내지 않으면, 팩은 재설정 이후
> 활동을 안정적으로 상관시킬 수 없다.** 앵커가 없으므로 활동이 "이후"라고 할
> 기준이 없다 — 그리고 이것은 AFTERLIFE-003을 먼저 침묵시키는데, 그 규칙이야말로
> 통제가 깨졌음을 알려주었을 규칙이다.
>
> **애플리케이션이 발급/계보 메타데이터를 보존하지 않으면, 탐지기는 새 액세스
> 토큰이 더 오래된 자격 증명에서 파생되었는지 판단할 수 없다.** 3초 전에 발급된
> 자격 증명은 한 달 된 체인에 의해 3초 전에 발급된 자격 증명과 구별할 수 없다.
이것들은 **선택적 로깅 환경설정이 아니다. 보안 탐지 요구사항이다.** 로그
볼륨을 줄이려고 `auth.token.issued`를 빼면 로깅이 저렴해지는 것이 아니라 탐지
하나가 꺼지는 것이다.
해결할 수 없는 활동은 조용히 버려지지 않고 **집계된다** —
`stats()["activity_with_unresolved_lineage"]`는 탐지기가 텔레메트리가 전혀
설명하지 않은 자격 증명에 대해 질의받을 때마다 0이 아니다. 아무 문제가 없어서
조용한 규칙과 눈이 멀어서 조용한 규칙은 밖에서 보면 똑같아 보이며, 그 숫자가
차이다.
또한 사실이고, 또한 문서화되어 있다:
* **탐지기는 요청을 거부할 수 없다.** 로그를 테일링한다. 격리가 실패했음을
알려줄 뿐, 격리하지는 않는다.
* **감사는 탐지가 아니라 스캔이다.** 휴면 생존자 격차를 메우지만, 누군가
실행할 때 실행된다. 페이지를 보낼 수 없다.
* **이 수정은 재설정 *이후*에 탈취된 자격 증명에는 도움이 되지 않는다.** 격리는
시점이지 속성이 아니다.
* **AFTERLIFE-003은 관찰된 격리를 검증하지, 아키텍처적 완전성을 검증하지
않는다.** 우연히 모든 것을 커버하는 디바이스 범위 폐기는 HIGH가 아니라 LOW
판정을 받는다. 버그는 여전히 거기 있다; 그 계정에서는 물지 않았을 뿐이다.
테스트는 "디바이스 두 개는 어떨까?"라고 물을 수 있지만, 로그는 실제로 일어난
일만 보고할 수 있다.
* **다른 지속성은 비밀번호 변경에서 완전히 살아남는다** — OAuth 그랜트, API
키, 메일 전달 규칙, 복구 연락처.
전체 논의: **[docs/limitations.md](https://github.com/het-p301204/afterlife/blob/main/docs/limitations.md)**.
---
## 랩 실행하기```bash
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt -r requirements-dev.txt
pytest -q
그런 다음, 순서대로:```bash python scripts/lab.py vulnerable
## 감사합니다!
이 프로젝트에 기여하고 싶으시다면, 이슈를 열거나 풀 리퀘스트를 제출해 주세요.```bash
python scripts/lab.py detect
이 프로젝트에 기여하고 싶으시다면, 이슈를 열거나 풀 리퀘스트를 제출해 주세요.```bash python scripts/lab.py fixed
## 감사합니다!
이 프로젝트가 유용하다고 생각하셨다면, GitHub에서 별을 눌러주시면 감사하겠습니다! ⭐
[](https://github.com/yourusername/yourrepo)
---
## 기여하기
기여를 환영합니다! 자세한 내용은 [CONTRIBUTING.md](https://github.com/het-p301204/afterlife/blob/main/CONTRIBUTING.md)를 참조해 주세요.
---
## 라이선스
이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다. 자세한 내용은 [LICENSE](https://github.com/het-p301204/afterlife/blob/main/LICENSE) 파일을 참조해 주세요.
---
## 면책 조항
이 도구는 교육 및 보안 연구 목적으로만 제공됩니다. 이 소프트웨어의 사용에 대한 모든 책임은 사용자에게 있습니다. 무단 시스템에 대한 무단 접근은 불법이며 비윤리적입니다. 항상 적절한 허가를 받고 책임감 있게 사용해 주세요.
---
## 연락처
- **작성자**: Your Name
- **GitHub**: [@yourusername](https://github.com/yourusername)
- **Twitter**: [@yourhandle](https://twitter.com/yourhandle)
---
**⭐ 이 저장소에 별을 눌러주시면 감사하겠습니다! ⭐**```bash
python scripts/lab.py audit
또는 교훈이 끝에 담긴 이야기 전체를 한 번에:```bash python scripts/lab.py all
그리고 그림:```bash
python scripts/lab.py console
커밋된 증거를 팩을 통해 재생합니다. 애플리케이션 실행은 필요하지 않습니다 — 탐지기는 엄격히 다운스트림에 있으며, 이것이 그 증명입니다:```bash python -m detector --once --timeline --events evidence/vulnerable-persistence.jsonl
한 번에 하나의 규칙, 또는 순진한 규칙, 또는 상태 스캔:```bash
python -m detector --once --rule AFTERLIFE-003 --events evidence/vulnerable-persistence.jsonl
alice / Password123! → changed during the demo to Correct-Horse-Battery-9!
가짜, 로컬, 그리고 이 저장소의 유일한 자격 증명.
---
## 보안 노트
**이 저장소에는 의도적으로 취약한 코드가 포함되어 있습니다.**
`app/auth.py:_revoke_for_security_change`는 `AFTERLIFE_MODE=vulnerable`일 때
의도적으로 폐기를 실패합니다.
취약한 경로는 **기본값이 아닙니다**. `AFTERLIFE_MODE`는 기본적으로 `fixed`이며,
`test_the_default_mode_is_the_safe_one`이 이를 고정합니다 — 아무것도 구성하지 않는 것을
잊어버리면 얻게 되는 망가진 동작을 가진 랩은 결국 실제 무언가로 복사될 것입니다.
어느 모드에서든 네트워크에 노출하기에 안전하지 않습니다: 베어러 자격 증명이 응답 본문에
반환되고, `GET /lab/credentials`는 서버가 보는 사용자의 자격 증명 상태 전체를 덤프하며,
`GET /lab/audit`는 설계상 인증이 없고, `POST /security-change`는 스스로에게 `admin`을
부여하며, 어디에도 TLS, 속도 제한, CSRF 보호 또는 계정 잠금이 없습니다. 모든 것은
`127.0.0.1`에 바인딩되며, `python -m app`과 `python -m console` 모두 다른 주소를
거부합니다.
이 저장소에는 실제 자격 증명, 키 또는 비밀이 나타나지 않습니다. 서명 키는 리터럴 문자열
`afterlife-lab-signing-key-not-a-secret-do-not-reuse`이고, 자격 증명 데이터베이스는
기본적으로 `:memory:`이며, 텔레메트리는 키 이름이 자격 증명처럼 보이는 값을 대체하고
자격 증명을 id와 지문으로 참조합니다. 세부 사항과 신고 절차:
**[SECURITY.md](https://github.com/het-p301204/afterlife/blob/main/SECURITY.md)**.
---
## 문서
| | |
|---|---|
| [docs/detection.md](https://github.com/het-p301204/afterlife/blob/main/docs/detection.md) | 규칙 팩: 세 가지 규칙 카드 모두, 상태 감사, 필수 텔레메트리, 오탐, 심각도 근거, 대응 런북 |
| [docs/tradeoffs.md](https://github.com/het-p301204/afterlife/blob/main/docs/tradeoffs.md) | 네 가지 폐기 아키텍처, JWT 긴장, 그리고 CVSS 논의 |
| [docs/limitations.md](https://github.com/het-p301204/afterlife/blob/main/docs/limitations.md) | 모든 사각지대, 수정이 수정하지 않는 것, 감사가 닫는 것, 그리고 랩 자체의 타협 |
| [docs/console-design.md](https://github.com/het-p301204/afterlife/blob/main/docs/console-design.md) | 콘솔이 왜 사망 등록부인지, 팔레트와 서체 결정, 그리고 잘려나간 것 |
| [docs/console-preview.html](https://github.com/het-p301204/afterlife/blob/main/docs/console-preview.html) | 콘솔, 하나의 자체 포함 파일로 구워짐 |
| [docs/figures/](https://github.com/het-p301204/afterlife/blob/main/docs/figures) | 이 README의 그림들, 페이로드에서 생성됨 |
| [report/AFTERLIFE-report.md](https://github.com/het-p301204/afterlife/blob/main/report/AFTERLIFE-report.md) | 취약한 실행에 대해 생성된 사고 보고서 |
| [SECURITY.md](https://github.com/het-p301204/afterlife/blob/main/SECURITY.md) | 로컬 전용 경계, 랩 자격 증명, 리댁션 계약 |
| [evidence/](https://github.com/het-p301204/afterlife/blob/main/evidence) | 정제된 샘플 텔레메트리, 그것이 생성하는 경보, 그리고 상태 감사 |
---
## 연구 참고 문헌
2026-09-11에 1차 출처에 대해 검증되었습니다. **VERIFIED**는 벤더 자체의 권고가
읽혔다는 뜻이고, **REPORTED**는 세부 사항이 벤더가 아닌 취약점 데이터베이스에서
왔다는 뜻입니다.
**표준 및 분류**
* [OWASP Top 10:2025 — A07 Authentication Failures](https://top10.owasp.org/2025/A07_2025-Authentication_Failures) — 정확히 이 실패를 설명합니다: "사용자 세션 또는 인증 토큰을 올바르게 무효화"하지 않는 애플리케이션. 그 CWE 매핑에는 아래 세 가지가 모두 포함됩니다.
* [RFC 9700](https://datatracker.ietf.org/doc/rfc9700/) — *OAuth 2.0 보안을 위한 최선의 현재 관행* (BCP 240, 2025년 1월). 이 랩이 기대는 두 가지: 회전된 리프레시 토큰의 재사용은 탈취를 시사하며(AFTERLIFE-002), **폐기는 현재 토큰뿐만 아니라 전체 토큰 패밀리를 무효화해야 합니다** — 이것이 IETF 관행으로 성문화된 AFTERLIFE-001의 논지입니다.
* [OWASP Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) — 자격 증명 및 보안 변경 후 세션 무효화
* [CWE-613: Insufficient Session Expiration](https://cwe.mitre.org/data/definitions/613.html) — *"공격자가 권한 부여를 위해 오래된 세션 자격 증명 또는 세션 ID를 재사용할 수 있게 한다"*. 아래 세 CVE가 모두 분류된 클래스입니다.
* [CWE-287: Improper Authentication](https://cwe.mitre.org/data/definitions/287.html)
* [CWE-384: Session Fixation](https://cwe.mitre.org/data/definitions/384.html) — 인접: 인증 상태 변경에도 살아남는 세션 식별자
**이 랩이 재현하는 패턴**
* **VERIFIED** — [GHSA-hvp3-26wx-g2w4](https://github.com/strapi/strapi/security/advisories/GHSA-hvp3-26wx-g2w4) / [CVE-2026-22706](https://github.com/advisories/GHSA-hvp3-26wx-g2w4), *Strapi: 비밀번호 재설정이 기존 리프레시 세션을 폐기하지 않음*. `@strapi/admin` 및 `@strapi/plugin-users-permissions` ≤ 5.33.2; 5.33.3에서 수정됨. CVSS v4.0 **2.1 (Low)**, `AV:N/AC:H/AT:N/PR:H/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N`. 2026-05-13 게시.
벤더 권고는 리프레시 토큰 무효화 단계가 호출자가 제공한 `deviceId`에
조건부였다고 밝히며, 패치는 `deviceId` 제공 여부와 관계없이 모든 비밀번호 변경
및 재설정 시 모든 리프레시 토큰을 무효화한다고 밝힙니다. 그 조건이
`_revoke_for_security_change`가 모델링하는 것이고, 그 패치가 fixed 모드가
구현하는 것입니다.
벤더 맥락: [Strapi 보안 공개, 2026년 5월](https://strapi.io/blog/security-disclosure-of-vulnerabilities-cve-2025-64526-cve-2026-22599-cve-2026-22706-cve-2026-22707-and-cve-2026-27886).
**같은 클래스, 다른 메커니즘**
* **REPORTED** — [CVE-2026-1163](https://nvd.nist.gov/vuln/detail/CVE-2026-1163), parisneo/lollms. 비밀번호 재설정 시 세션 무효화가 *전혀* 없으며, 기본 세션 수명은 31일입니다. 이 버그의 단순한 버전 — 그리고 순진한 탐지기가 실제로 잡을 수 있는 버전입니다.
* **REPORTED** — [CVE-2026-40934](https://nvd.nist.gov/vuln/detail/CVE-2026-40934), Jupyter Server ≤ 2.17.0 (2.18.0에서 수정됨). 쿠키 서명 비밀이 정적 파일에 영속화되고 비밀번호 변경 시 회전되지 않으므로, 재설정 전에 발급된 쿠키는 그 이후에도 암호학적으로 유효하게 유지됩니다. 같은 결과에 이르는 세 번째 경로: 여기서 *자격 증명*이 유효한 것은 *키*가 결코 변경되지 않았기 때문이며, 이는 [docs/tradeoffs.md](https://github.com/het-p301204/afterlife/blob/main/docs/tradeoffs.md)의 Option D의 실패 모드입니다.
세 제품, 세 메커니즘 — 조건부 매개변수, 누락된 호출, 회전되지 않은 키 — 하나의 결과:
재설정은 성공했고 공격자는 남았습니다.
---
<div align="center">
**공격자가 탐지되지 않는 것은 그들의 요청이 악의적으로 보이기 때문이 아닙니다.**
그들이 만든 모든 요청은 잘 형성되었고, 올바르게 서명되었으며, 서버가 방금 그들을 위해
발급한 자격 증명을 지니고 있었습니다.
**그들이 탐지되는 것은 보안 이벤트에서 죽었어야 할 자격 증명 계보가 그 이후에
수용되었기 때문입니다.**
<br>
<sub>MIT · 로컬 랩 · 여기 있는 어떤 것도 배포하기에 안전하지 않습니다</sub>
</div>
| 규칙 | 심각도 | 답하는 질문 | 발동 시점 |
|---|
| AFTERLIFE-002 | Refresh credential reuse | CRITICAL / MEDIUM | 탈취되었는가? | 재사용 시점 |
| AFTERLIFE-003 | Incomplete revocation at a security change | HIGH / LOW | 격리가 실행되었는가? | 변경 시점 — 공격자 불필요 |
| AFTERLIFE-001 | Post-revocation credential lineage use | HIGH | 오래된 계보가 사용되었는가? | 최초 수용 시점 |
| RULE PACK |
| 시나리오 | 모드 | 이벤트 | 최악 | 발동된 규칙 | stale | dormant |
|---|
legitimate-only | vulnerable | 19 | LOW | AFTERLIFE-003 | 0 | 0 |
legitimate-only | fixed | 19 | – | none | 0 | 0 |
stolen-refresh | vulnerable | 22 | HIGH | AFTERLIFE-001, AFTERLIFE-003 | 1 | 0 |
stolen-refresh | fixed | 23 | – | none | 0 | 0 |
stolen-refresh-with-device-id | vulnerable | 19 | LOW | AFTERLIFE-003 | 0 | 0 |
stolen-refresh-with-device-id | fixed | 19 | – | none | 0 | 0 |
multi-device | vulnerable | 29 | HIGH | AFTERLIFE-001, AFTERLIFE-003 | 2 | 0 |
multi-device | fixed | 29 | – | none | 0 | 0 |
refresh-reuse | vulnerable | 11 | MEDIUM | AFTERLIFE-002 | 0 | 0 |
refresh-reuse | fixed | 15 | MEDIUM | AFTERLIFE-002 | 0 | 0 |
dormant-survivor | vulnerable | 15 | HIGH | AFTERLIFE-003 | 1 | 1 |
dormant-survivor | fixed | 19 | – | none | 0 | 0 |
mfa-change | vulnerable | 18 | HIGH | AFTERLIFE-001, AFTERLIFE-003 | 1 | 0 |
mfa-change | fixed | 18 | – | none | 0 | 0 |
account-recovery | vulnerable | 18 | HIGH | AFTERLIFE-001, AFTERLIFE-003 | 1 | 0 |
account-recovery | fixed | 18 | – | none | 0 | 0 |
expired-lineage | vulnerable | 17 | LOW | AFTERLIFE-003 | 0 | 0 |
expired-lineage | fixed | 20 | – | none | 0 | 0 |
| 사례 | 결과 | 이유 |
|---|
| 비밀번호 변경 후, 대체 세션이 사용됨 | 경보 없음 | 새 계보의 루트가 워터마크와 같거나 이후임 |
재설정 후 즉시 브라우징 (/me, /profile, /settings) | 경보 없음 | 하나의 새 계보, 하나의 새 루트 |
| 폰, 노트북, 태블릿, 모두 올바르게 회전됨 | 경보 없음 | 각 로그인은 자체 계보 |
| 변경 이후에 로그인한 기기가 공격자와 나란히 브라우징 | 경보 없음 | 새 루트 — 그리고 stale 계보는 여전히 단독으로 경보 |
| 거부된 stale 자격 증명 | 경보 없음 | result: failure는 제외됨; 그것은 방어를 위한 증거 |
| 컴포넌트 간 최대 2초의 클록 스큐 | 경보 없음 | 문서화된 허용 오차 |
| 실패한 비밀번호 변경 | 경보 없음 | 앵커가 아님 |
| 변경 이전의 활동 | 경보 없음 | 시간 순서 검사 |
회전된 자격 증명 (reason: rotated) | 사망으로 계산되지 않음 | 소비된 자격 증명이지, 죽은 계보가 아님 |
| 자격 증명이 만료된 40일 된 세션 | 생존자 아님 | 만료는 계보별로 추적됨 |
| 완전히 새 계정의 첫 비밀번호 변경 | 경보 없음 | 들어갈 때 살아 있던 것이 없음 |
| stale 계보로부터의 리프레시 토큰 회전 | 경보 | 새 타임스탬프, stale 혈통 — 이것이 발견임 |
| 비밀번호 변경 / 재설정 | 세션이 수립된 비밀이 사라졌다 |
| MFA 등록 또는 변경 | 세션이 수립된 요소가 계정의 요소가 아니다 |
| 역할 변경 / 권한 상승 | 자격 증명이 다른 권한 부여 아래에서 발급되었다 |
| 계정 복구 | 구조상, 계정이 방금 전까지 다른 사람의 손에 있었을 수 있다 |
| 로그인 | 다른 것들을 폐기하지 않는다 — 새로운 계보이지, 기존 것들이 신뢰할 수 없다는 진술이 아니다 |
| 메커니즘 | 얻는 것 | 비용 |
|---|
| A | 사용자별 폐기 워터마크 | 한 번의 쓰기로 모든 것을 폐기하며, 서버가 발급했음을 잊어버린 자격 증명까지 포함한다; O(1) 저장 및 검사 | 읽기 경로에 서버 측 상태; 타임스탬프 의미가 정확히 맞아야 한다; 워터마크 이후 발급된 자격 증명에 대해서는 아무것도 말하지 않는다 |
| B | 단명 액세스 + 폐기 가능한 리프레시 | 읽기 경로 상태 없이 액세스 토큰 피해를 제한한다 | 탈취된 액세스 토큰은 만료될 때까지 유효하다; 리프레시 측은 여전히 상태가 필요하다 — 이것이 CVE가 자리 잡고 있는 패턴이다 |
| C | 명시적 거부 목록 | 정밀하다; 사고 대응자가 읽을 수 있다; 격리를 증명하는 텔레메트리를 생성한다 | 상태가 증가하고 정리가 필요하다; 열거하기로 기억한 것만 폐기한다 — 버그가 잘못 처리한 바로 그 쿼리다 |
| D | 완전 무상태 JWT | 읽기 경로 상태가 전혀 없다 | 폐기가 없다. TTL과 키 교체가 유일한 수단이다 |
python -m detector --once --naive --events evidence/vulnerable-persistence.jsonl
## 감사합니다!
이 프로젝트에 기여하고 싶으시다면, 이슈를 열거나 풀 리퀘스트를 제출해 주세요.```bash
python -m detector --audit --events evidence/vulnerable-persistence.jsonl
CLI는 무언가를 발견하면 1을 반환하므로, 출력을 파싱하지 않고도 CI 검사로 사용할 수 있습니다.
두 구현 모두에 대해 아홉 가지 시나리오를 자체 검증 불변 조건과 함께 실행합니다:```bash python scripts/scenarios.py
사고 대응자가 받게 될 사고 보고서와 이 README의 수치:```bash
python scripts/report.py
이 프로젝트는 다음의 지원과 기여 없이는 불가능했을 것입니다:
기여를 환영합니다! 다음 단계를 따라주세요:
git checkout -b feature/amazing-feature)git commit -m 'Add some amazing feature')git push origin feature/amazing-feature)자세한 내용은 CONTRIBUTING.md를 참조하세요.
이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다 — 자세한 내용은 LICENSE 파일을 참조하세요.
이 도구는 교육 및 윤리적 보안 테스트 목적으로만 제공됩니다. 이 소프트웨어의 사용은 귀하의 책임입니다. 사용자는 다음을 준수할 책임이 있습니다:
개발자는 이 소프트웨어의 오용이나 남용에 대해 책임을 지지 않습니다.
⭐ 이 프로젝트가 유용하다고 생각되면 스타를 눌러주세요!```bash python scripts/figures.py
라이브 콘솔과 랩 API를 직접 사용하는 방법 — 둘 다 루프백 전용:```bash
python -m console
이 프로젝트에 기여하고 싶으시다면, 기여 가이드라인을 확인해 주세요.```bash python -m app --mode vulnerable --port 9101
커밋된 텔레메트리를 재생성합니다(고정된 클럭, 실행 간 바이트 단위로 동일):```bash
python scripts/lab.py evidence
작업 러너, 어느 쪽이든 동일한 타깃:```bash make demo
## 감사의 말
이 프로젝트는 다음의 지원을 받았습니다:
- **NLnet Foundation** - [NGI Zero Core](https://nlnet.nl/core) 프로그램을 통한 지원
- **Open Technology Fund** - [Internet Freedom Fund](https://www.opentech.fund/funds/internet-freedom-fund/)을 통한 지원
- **GitHub** - [GitHub Secure Open Source Fund](https://github.com/open-source/github-secure-open-source-fund)를 통한 지원
## 기여하기
기여를 환영합니다! 자세한 내용은 [CONTRIBUTING.md](https://github.com/het-p301204/afterlife/blob/main/CONTRIBUTING.md)를 참조하세요.
## 라이선스
이 프로젝트는 [GNU General Public License v3.0](https://github.com/het-p301204/afterlife/blob/main/LICENSE)에 따라 라이선스가 부여됩니다.
## 연락처
- **웹사이트**: [https://kitploit.com](https://kitploit.com)
- **이메일**: [[email protected]](mailto:[email protected])
- **트위터**: [@KitPloit](https://twitter.com/KitPloit)
---
**KitPloit** - *해커를 위한 최고의 보안 도구*```powershell
./make.ps1 demo