
रिवोकेशन पर्सिस्टेंस डिटेक्शन लैब: जब पासवर्ड रीसेट सफल होता है लेकिन हमलावर कभी नहीं जाता। Strapi CVE-2026-22706 कंडीशनल-रिवोकेशन बग, उसका फिक्स, एक तीन-नियम डिटेक्शन पैक, और वह नैव नियम जो इसे मिस कर देता है, को रिप्रोड्यूस करता है।
जब पासवर्ड रीसेट सफल हो जाता है लेकिन हमलावर कभी नहीं जाता।
एक स्थानीय red/blue प्रयोगशाला एक भेद्यता वर्ग के लिए: वह क्रेडेंशियल जो उस घटना से अधिक जीवित रहता है जिसे उसे समाप्त करना था। यह exploit, मूल कारण, समाधान, एक तीन-नियम पहचान पैक, उन चीज़ों के लिए एक स्थिति ऑडिट जो नियम संरचनात्मक रूप से नहीं देख सकते, एक फोरेंसिक कंसोल — और वह पहचान नियम जो काम नहीं करता, को रिपॉज़िटरी में रखा गया है ताकि उसे विफल होते हुए प्रदर्शित किया जा सके।
त्वरित शुरुआत · निष्कर्ष · सामान्य पहचान क्यों विफल होती है · नियम पैक · कंसोल · समाधान · मैट्रिक्स · परीक्षण · दस्तावेज़
वही हमला। वही अनुरोध। एक अंतर।
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}
कोई त्रुटि नहीं। कोई विसंगति नहीं। गिनने के लिए कोई विफल प्रमाणीकरण नहीं। हमलावर का एक्सेस क्रेडेंशियल *तीन सेकंड पुराना* है और इसे सर्वर द्वारा, अनुरोध पर, रीसेट के बाद जारी किया गया था।
यहाँ पूरी भेद्यता है:```python
def _revoke_for_security_change(self, user, kind, device_id):
if device_id:
revoke_credentials(user, device_id=device_id) # ← the finding
इसे एक समीक्षक की तरह पढ़ें। Revocation logic ठीक वहीं है। यह सही scope के साथ सही function को call करता है। इसके आस-पास का endpoint password hash को update करता है, 200 return करता है, और एक नया session issue करता है — एक सही password change का हर observable behaviour मौजूद है।
और अगर caller device_id छोड़ देता है, तो कुछ भी revoke नहीं होता, और endpoint फिर भी success report करता है।
यह कोई काल्पनिक स्थिति नहीं है। यह Strapi ≤ 5.33.2 में
CVE-2026-22706
है, जहाँ refresh-token invalidation step caller-supplied deviceId पर conditional था। स्कोर 2.1, Low। इस पर
नीचे चर्चा की गई है।
स्कोर इसलिए कम है क्योंकि attacker के पास पहले से ही access था — यही entry condition है, और यह bug कुछ नया नहीं देता। जो यह देता है वह है duration, और यह ऐसा उस एक control को तोड़कर करता है जिसे victim स्वयं operate कर सकता है।
test_persistence_lasts_as_long_as_the_refresh_credential इसे दिखाने के लिए सात दिनों का simulated time चलाता है।Containment path में Low-scored bug की कीमत feature path में Low-scored bug से ज़्यादा होती है, क्योंकि कीमत incident के दौरान चुकाई जाती है, जब कोई advisories नहीं पढ़ रहा होता।
जो rule आप पहले लिखते हैं:```text IF credential.issued_at < credential_change.timestamp: ALERT
यह कोई बेवकूफ़ी भरा नियम नहीं है। यह सस्ता है, एक फ़ील्ड चाहिए, समस्या की परिभाषा जैसा पढ़ा जाता है, और यह **साधारण मामले के बारे में सही है** — रीसेट के बाद चोरी हुए *access* टोकन का उपयोग करने वाला हमलावर पकड़ा जाता है।
`test_the_naive_rule_catches_the_simple_case` यह दावा करता है कि यह काम करता है।
दोनों नियम एक ही क्रेडेंशियल के बारे में एक ही सवाल पूछते हैं। वे जवाब देने के लिए अलग-अलग फ़ील्ड पढ़ते हैं, और दोनों फ़ील्ड असहमत हैं:

**तीन सेकंड बाद, या एक घंटा पहले — एक ही क्रेडेंशियल, एक ही क्षण में।** naive नियम हमलावर के नियंत्रण वाला फ़ील्ड पूछता है, और एक `POST /refresh` उसे रीसेट कर देता है।
इसे request log पर चलाएँ, जहाँ यह क्वेरी वास्तव में लिखी जाती है, क्योंकि request logs ही आपके पास होते हैं:```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.
यहीं रुक जाना आसान होता, और बेईमानी भी, इसलिए लैब naive नियम का सबसे निष्पक्ष संस्करण भी चलाती है — जिसे refresh endpoint को भी देखने के लिए विस्तारित किया गया है:```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
उसी क्षण सक्रिय होता है जब परिवर्तन लागू होता है, हमलावर के कुछ भी छूने से तीन सेकंड पहले, क्योंकि इसके लिए आवश्यक साक्ष्य पहले से ही पूर्ण होते हैं: लॉग बताता है कि कौन-कौन से वंश (lineages) प्रवेश के समय सक्रिय थे, और यह नहीं बताता कि उन्हें रद्द (revoked) किया गया था।
इसे किसी पीड़ित या शोषण की आवश्यकता नहीं है। यह किसी भी उपयोगकर्ता द्वारा किए गए पहले पासवर्ड रीसेट पर ही दोष की रिपोर्ट कर देगा — जो इसे वह बनाता है जिसे आप स्टेजिंग में चलाते हैं, जहाँ प्रतीक्षा करने के लिए कोई हमलावर नहीं होता। AFTERLIFE-001 आपको बताता है कि उल्लंघन (breach) जारी है; AFTERLIFE-003 आपको बताता है कि आपका कंटेनमेंट नियंत्रण टूटा हुआ है।

`application recorded watermark: no` वह फ़ील्ड है जो किसी घटना प्रतिक्रियाकर्ता (incident responder) को लक्षण के बजाय *दोष* की ओर इंगित करता है।
### नियम जिस वॉटरमार्क का उपयोग करते हैं वह वह नहीं है जिसकी अनुप्रयोग रिपोर्ट करता है
क्रेडेंशियल-परिवर्तन इवेंट में एक `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 में लाल का अर्थ है कोई त्रुटि हुई। यहाँ कुछ भी त्रुटि नहीं करता — vulnerable रन में हर अनुरोध 200 लौटाता है। इसलिए रंग मृत्यु-दर का अनुसरण करता है: ठंडा वह क्रेडेंशियल है जो मर गया जब उसे मरने को कहा गया, गर्म वह है जो अब भी साँस ले रहा है, और गिलोटिन के पार, अब भी गर्म होना ही पूरा निष्कर्ष है।
(BLACKOUT ने वही परिपाटी अपनाई, जहाँ 200 लाल वाला था।)
पतली ऊर्ध्वाधर बूँदें वंशावली हैं: उसी क्षण अपने जनक से ढाला गया संतान क्रेडेंशियल। rt-001 → rt-002 → rt-004 पूरी विंडो में नीचे और दाईं ओर क़दम बढ़ाता है, और vulnerable मोड में यह कट के बाद भी क़दम बढ़ाता रहता है — जो किसी वंशावली के अपने ही विलोपन-घटना के दूसरी ओर नए क्रेडेंशियल ढालने का चित्र है।```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 परिवर्तन पर और AFTERLIFE-001 तीन
सेकंड बाद। डिज़ाइन तर्क, जिसमें क्या काटा गया, यह भी शामिल है,
docs/console-design.md में है।
कंसोल निर्माण से ही केवल-पठन है। यह पूर्ण हो चुके परिदृश्यों को फिर से चलाता है और उन्हें चित्रित करता है; यह कार्यान्वयन को स्विच नहीं कर सकता, घड़ी को हिला नहीं सकता या कुछ भी रद्द नहीं कर सकता।
test_the_console_has_no_control_surfaceयह दावा करता है कि एकमात्र गैर-GET रूट/api/rebuildहै। एक पठन सतह जो जो पढ़ रही है उसे बदल सकती है, वह भरोसेमंद नहीं हो सकती।
छह पैकेज, एक सुरक्षा वर्ग, कोई अवसंरचना नहीं।```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
The application runs **in-process** under the demo, the console और tests। No
ports, no dev server, no Docker। The clock pinned है, so every run
deterministic है और committed telemetry, console, figures और reports सभी
runs के बीच byte-identical हैं — जिसे CI `git diff --exit-code` के साथ check करता है।
### Server-side enforcement
A credential एक **row** है, string नहीं। JWT जो client hold करता है वह `cid` carry करता है —
एक lookup key — और row decide करती है कि credential alive है या नहीं।
Expiry को row के `expires_at` के against check किया जाता है, न कि `exp` claim के against जो
holder ने present किया। Both temporal claims PyJWT को `verify=False` के साथ handed हैं,
जिसका यहाँ मतलब है *"the server checks this itself"*, और [`app/auth.py`](https://github.com/het-p301204/afterlife/blob/main/app/auth.py)
हर request पर ऐसा करता है। Client को कभी vote नहीं मिलता कि उसका own
credential अभी भी valid है या नहीं।
---
## Attack timeline```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 शाखा ही पूरा मुद्दा है। हमलावर पुरानी credential का उपयोग नहीं करता। वे एक पुरानी lineage का उपयोग करते हैं, और lineage मांग पर कुछ नया mint करती है।
एक session किसी lineage का root होता है। उसके नीचे mint की गई हर चीज़ उस root को हमेशा के लिए inherit करती है।```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` तीन सेकंड पुराना है। इसका वंश एक घंटे पुराना है। दोनों तथ्य सत्य हैं,
और इनमें से केवल एक ही अनुरोध लॉग में दिखाई देता है।
दो अपरिवर्तनीय नियम इसे एक साथ बनाए रखते हैं, और दोनों का परीक्षण किया गया है:
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
| -s | --server | Server URL (default: http://localhost:8080) |
| -t | --token | API token for authentication |
| -o | --output | Output file path |
| -f | --format | Output format: json, yaml, table |
| -v | --verbose | Enable verbose logging |
| -q | --quiet | Suppress non-essential output |
| | | Show help message |
| | | Show version information |
# Scan a single target
scanner scan --target example.com
# Scan multiple targets from a file
scanner scan --input targets.txt --output results.json
# Use a custom configuration file
scanner scan --config /path/to/config.yaml
# Run with verbose output
scanner scan --target example.com --verbose
The scanner can be configured using a YAML configuration file. By default, it looks for config.yaml in the current directory.
# Scanner configuration
server:
url: "http://localhost:8080"
timeout: 30
scan:
threads: 10
timeout: 60
retries: 3
output:
format: "json"
directory: "./results"
The scanner exposes a REST API that can be used to integrate with other tools.
# Start a scan
curl -X POST http://localhost:8080/api/v1/scan \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target": "example.com"}'
# Get scan status
curl http://localhost:8080/api/v1/scan/SCAN_ID \
-H "Authorization: Bearer YOUR_TOKEN"
Contributions are welcome! Please follow these steps:
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)This project is licensed under the MIT License - see the LICENSE file for details.```bash python scripts/lab.py audit
| `--no-color` | Disable colored output |
| `--debug` | Enable debug mode |
| `--verbose` | Enable verbose mode |
| `--silent` | Enable silent mode |
| `--version` | Show version and exit |
| `--help` | Show help and exit |```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
दो स्रोत, जानबूझकर: telemetry ऑडिट देखता है कि लॉग क्या साबित कर सकता है, server-state ऑडिट देखता है कि डेटाबेस क्या मानता है। जहाँ वे असहमत होते हैं, लॉग क्रेडेंशियल स्थिति का एक विश्वसनीय रिकॉर्ड नहीं है, और उस पर बनाई गई हर डिटेक्शन दिखने से कमज़ोर होती है — इसलिए कमांड दोनों को प्रिंट करता है और यदि वे भिन्न हों तो ऐसा कहता है।
server-state ऑडिट को 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 द्वारा जाँचा गया: फिक्स किसी भी
परिदृश्य में कोई control-failure finding उत्पन्न नहीं करता। AFTERLIFE-002 को गुजरने
दिया जाता है — यह एक चोरी की रिपोर्ट करता है, control failure की नहीं, और एक सही
कार्यान्वयन में भी रिपोर्ट करने के लिए चोरियाँ होती हैं।
एक नियम जो हर पासवर्ड रीसेट पर पेज भेजता है, एक सप्ताह के भीतर म्यूट कर दिया जाता है, और एक म्यूट किया गया नियम किसी भी नियम से बदतर है — यह एक ऐसा नियम है जिसे हर कोई मानता है कि चल रहा है।
प्रत्येक पंक्ति का एक टेस्ट है
tests/test_detector_afterlife001.py और
tests/test_detector_rulepack.py में।
2-सेकंड सहनशीलता पर। इस लैब में प्रत्येक टाइमस्टैम्प एक प्रक्रिया और एक घड़ी से
आता है, इसलिए ईमानदार सहनशीलता शून्य है। 2s वह है जो NTP के अंतर्गत एक यथार्थवादी
दो-होस्ट परिनियोजन को चाहिए, और यह इस हमले द्वारा उत्पन्न अंतरालों से तीन कोटि छोटा
है — एक refresh क्रेडेंशियल का पूरा उद्देश्य ही दीर्घायु होना है। सहनशीलता एक ऐसी विंडो
है जिसमें एक वास्तव में stale lineage को अनदेखा किया जाता है, इसलिए इसे जानबूझकर छोटा
रखा गया है: test_12b_a_gap_beyond_the_tolerance_does_alert यह पिन करता है कि 2.1s अभी
भी फायर करता है।
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`, प्रत्येक अनुरोध पर क्रेडेंशियल के स्वयं के जारी होने *और* उसके lineage रूट दोनों के विरुद्ध तुलना किया जाता है:```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 | यह क्या pin करता है |
|---|---|
| [`test_fixed_mode.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_fixed_mode.py) | **वह regression suite जो कभी red नहीं होनी चाहिए** — revocation event से पहले जारी कोई भी credential उसके बाद स्वीकार नहीं किया जाता, जिसमें एक stale lineage से उत्पन्न credential भी शामिल है |
| [`test_vulnerable_mode.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_vulnerable_mode.py) | vulnerability **मौजूद है**, deterministic है, 30 दिन तक रहती है, और conditional के कारण होती है — `device_id` control यह साबित करता है कि revocation path optional है, dead नहीं |
| [`test_lineage.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_lineage.py) | एक refresh chain एक lineage और एक root रखती है; केवल login एक lineage बनाता है |
| [`test_detector_afterlife001.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_detector_afterlife001.py) | 7 detection cases, 9 false-positive cases, 2 blind spots, सभी 24 event orderings, bounded state, malformed input |
| [`test_detector_rulepack.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_detector_rulepack.py) | AFTERLIFE-002 और AFTERLIFE-003 — हर verdict, grace window, rotation-is-not-death, expiry exclusion, और engine की ordering |
| [`test_naive_detector.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_naive_detector.py) | NAIVE-001 **उसी विशिष्ट तरीके से** fail होता रहता है जिसका दावा यह README करता है — जिसमें यह भी शामिल है कि उसके पास आवश्यक data था |
| [`test_audit_and_console.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_audit_and_console.py) | वह dormant survivor जिसे कोई rule नहीं देख सकता; दोनों audit sources का सहमत होना; console का अपना कोई verdict न निकालना; offline preview का कुछ भी fetch न करना; इस README का हर figure in bounds, stylesheet-free और byte-stable होना |
| [`test_scenarios.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_scenarios.py) | वे matrix rows जो एक argument रखती हैं |
| [`test_privilege_changes.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_privilege_changes.py) | MFA, role change और account recovery, दोनों modes |
| [`test_telemetry.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_telemetry.py) | redaction contract, और जारी किए गए literal bearer strings के लिए log का grep |
| [`test_app.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_app.py) | forged signatures, गलत credential types, server state से expiry |
| [`test_infrastructure.py`](https://github.com/het-p301204/afterlife/blob/main/tests/test_infrastructure.py) | config, clock, tailer partial lines, दोनों CLIs, हर demo command |
Suite का सबसे मजबूत test है
`test_no_bearer_string_or_password_ever_reaches_the_log`: यह पूरा attack चलाता है
और फिर log file में application द्वारा दिए गए actual credentials खोजता है, साथ ही
दोनों passwords, और signing key भी। Field names के लिए नहीं — values के लिए।
---
## Blind spots
स्पष्ट रूप से बताए गए हैं, क्योंकि इन्हें छिपाना lab को बेईमान बना देगा। प्रत्येक का एक test है।
> **यदि application credential-change events emit नहीं करता, तो pack post-reset
> activity को विश्वसनीय रूप से correlate नहीं कर सकता।** कोई anchor नहीं है, इसलिए
> activity के "after" होने के लिए कुछ नहीं है — और यह पहले AFTERLIFE-003 को चुप करा
> देता है, जो वह rule है जो आपको बताता कि control टूटा हुआ था।
>
> **यदि application issuance/lineage metadata retain नहीं करता, तो detector यह
> निर्धारित नहीं कर सकता कि कोई fresh access token किसी पुराने credential से
> उत्पन्न हुआ था या नहीं।** तीन सेकंड पहले mint किया गया credential, एक महीने पुरानी
> chain द्वारा तीन सेकंड पहले mint किए गए credential से अलग नहीं पहचाना जा सकता।
ये **optional logging preferences नहीं हैं। ये security detection requirements हैं।**
Log volume कम करने के लिए `auth.token.issued` हटाना logging को सस्ता नहीं बनाता;
यह एक detection बंद कर देता है।
Unresolvable activity को **counted** किया जाता है, चुपचाप drop नहीं किया जाता —
`stats()["activity_with_unresolved_lineage"]` तब non-zero होता है जब भी detector से
ऐसे credentials के बारे में पूछा गया जिन्हें telemetry ने कभी describe नहीं किया।
एक rule जो इसलिए चुप है क्योंकि कुछ गलत नहीं है और एक rule जो इसलिए चुप है क्योंकि
वह blind है, बाहर से एक जैसे दिखते हैं, और वह संख्या यही अंतर है।
यह भी सत्य है, और यह भी documented है:
* **Detector किसी request को refuse नहीं कर सकता।** यह एक log tail करता है। यह
आपको बताता है कि containment fail हुआ; यह contain नहीं करता।
* **Audit एक scan है, detection नहीं।** यह dormant-survivor gap को बंद करता है,
लेकिन यह तब चलता है जब कोई इसे चलाता है। यह page नहीं कर सकता।
* **Fix reset के *बाद* चुराए गए credentials में मदद नहीं करता।** Containment एक
point in time है, कोई property नहीं।
* **AFTERLIFE-003 observed containment verify करता है, architectural completeness
नहीं।** एक device-scoped revocation जो संयोग से सब कुछ cover कर लेती है, उसे LOW
finding मिलती है, HIGH नहीं। Bug अभी भी वहीं है; उस account पर उसने काटा नहीं।
एक test पूछ सकता है "दो devices के बारे में क्या?"; एक log केवल वही report कर सकता
है जो वास्तव में हुआ।
* **अन्य persistence password change के बाद पूरी तरह जीवित रहती है** — OAuth grants,
API keys, mail-forwarding rules, recovery contacts।
पूरी चर्चा: **[docs/limitations.md](https://github.com/het-p301204/afterlife/blob/main/docs/limitations.md)**।
---
## Running the lab```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
| `-s` | `--server` | Server URL (default: `http://localhost:8080`) |
| `-t` | `--token` | API token for authentication |
| `-o` | `--output` | Output file path |
| `-f` | `--format` | Output format: `json`, `yaml`, `table` |
| `-v` | `--verbose` | Enable verbose output |
| `-q` | `--quiet` | Suppress non-essential output |
| `-h` | `--help` | Show help message |
| `-V` | `--version` | Show version information |
### उदाहरण
```bash
# List all projects
kitploit-cli projects list
# Get details of a specific project
kitploit-cli projects get --id 12345
# Create a new scan
kitploit-cli scans create --target https://example.com --profile full
# Export results to JSON
kitploit-cli scans results --id 67890 --format json --output results.json
The CLI reads configuration from ~/.kitploit/config.yaml:
server: http://localhost:8080
token: your-api-token-here
output_format: table
verbose: false
Environment variables can override configuration file settings:
The CLI returns the following exit codes:
For detailed error messages, use the --verbose flag.```bash
python scripts/lab.py detect
| `--no-color` | Disable colored output |
| `--debug` | Enable debug mode |
| `--verbose` | Enable verbose mode |
| `--silent` | Enable silent mode |
| `--version` | Show version |
| `--help` | Show help |```bash
python scripts/lab.py fixed
| --no-color | रंगीन आउटपुट अक्षम करें |
| --debug | डिबग लॉगिंग सक्षम करें |
| --verbose | विस्तृत लॉगिंग सक्षम करें |
| --silent | मौन मोड सक्षम करें |
| --version | संस्करण जानकारी दिखाएं |
| --help | सहायता संदेश दिखाएं |```bash
python scripts/lab.py audit
या पूरी कहानी एक साथ, अंत में सीख के साथ:```bash
python scripts/lab.py all
और चित्र:```bash python scripts/lab.py console
<details>
<summary><b>बाकी सब कुछ</b> — डिटेक्टर CLI, मैट्रिक्स, रिपोर्ट, आंकड़े, सर्वर</summary>
<br>
कमिट किए गए साक्ष्य को पैक के माध्यम से रीप्ले करें। किसी एप्लिकेशन रन की आवश्यकता नहीं — डिटेक्टर पूरी तरह से डाउनस्ट्रीम है, और यह प्रमाण है:```bash
python -m detector --once --timeline --events evidence/vulnerable-persistence.jsonl
एक बार में एक नियम, या भोला वाला, या स्थिति स्कैन:```bash python -m detector --once --rule AFTERLIFE-003 --events evidence/vulnerable-persistence.jsonl
| `-s` | `--server` | Server URL (default: `http://localhost:8080`) |
| `-t` | `--token` | API token for authentication |
| `-o` | `--output` | Output file path |
| `-f` | `--format` | Output format: `json`, `yaml`, `table` |
| `-v` | `--verbose` | Enable verbose logging |
| `-q` | `--quiet` | Suppress non-error output |
| `-h` | `--help` | Show help message |
| `-V` | `--version` | Show version information |
### उदाहरण
```bash
# Scan a single target
scanner scan --target example.com
# Scan multiple targets from a file
scanner scan --input targets.txt --output results.json
# Use a custom configuration
scanner scan --config /path/to/config.yaml --verbose
The scanner reads its configuration from ~/.scanner/config.yaml by default. You can override this with the --config flag.
server:
url: http://localhost:8080
token: your-api-token
timeout: 30s
scan:
concurrency: 10
rate_limit: 100
retries: 3
output:
format: json
directory: ./results
The REST API is available at /api/v1/. All endpoints require authentication via the Authorization: Bearer <token> header.
The scanner returns the following exit codes:
Contributions are welcome! Please read the CONTRIBUTING.md file for guidelines.
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)This project is licensed under the MIT License - see the LICENSE file for details.
| `--no-color` | Disable colored output |
| `--debug` | Enable debug mode |
| `--verbose` | Enable verbose mode |
| `--silent` | Enable silent mode |
| `--version` | Show version |
| `--help` | Show help |```bash
python -m detector --audit --events evidence/vulnerable-persistence.jsonl
CLI कुछ पाता है तो 1 के साथ बाहर निकलता है, इसलिए यह अपने आउटपुट को पार्स किए बिना CI चेक के रूप में काम करता है।
दोनों कार्यान्वयनों के विरुद्ध नौ परिदृश्य, स्व-जाँच अपरिवर्तनीयों के साथ:```bash python scripts/scenarios.py
जिस घटना रिपोर्ट को एक प्रतिक्रियाकर्ता को सौंपा जाएगा, और इस README में दिए गए आंकड़े:```bash
python scripts/report.py
| -s | --server | Server URL (default: http://localhost:8080) |
| -t | --token | Authentication token |
| -o | --output | Output file path |
| -f | --format | Output format: json, yaml, table |
| -v | --verbose | Enable verbose logging |
| -q | --quiet | Suppress non-error output |
| | | Show help message |
# Scan a single target
scanner -t example.com
# Scan multiple targets from a file
scanner -f targets.txt -o results.json
# Use a custom server with authentication
scanner -s https://api.example.com -t YOUR_TOKEN -v
The tool reads configuration from ~/.scanner/config.yaml:
server: http://localhost:8080
token: your-token-here
timeout: 30
threads: 10
Results can be exported in multiple formats:
Common error codes and their meanings:
We welcome contributions! Please follow these steps:
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)This project is licensed under the MIT License - see the LICENSE file for details.```bash python scripts/figures.py
लाइव कंसोल, और लैब API हाथ से — दोनों केवल लूपबैक:```bash
python -m console
| -s | --server | Server URL (default: http://localhost:8080) |
| -t | --token | API token for authentication |
| -o | --output | Output file path |
| -f | --format | Output format (json, yaml, table) |
| -v | --verbose | Enable verbose output |
| -q | --quiet | Suppress non-essential output |
| | | Show help message |
# Scan a single target
scanner scan --target example.com
# Scan multiple targets from a file
scanner scan --input targets.txt --output results.json
# Use a custom configuration
scanner scan --config custom-config.yaml --verbose
The scanner can be configured using a YAML configuration file:
server:
url: "http://localhost:8080"
token: "your-api-token"
timeout: 30
scan:
threads: 10
timeout: 60
retries: 3
output:
format: "json"
directory: "./results"
POST /api/v1/scanStart a new scan.
अनुरोध:
{
"target": "example.com",
"options": {
"depth": 3,
"timeout": 60
}
}
प्रतिक्रिया:
{
"id": "scan-12345",
"status": "running",
"created_at": "2024-01-15T10:30:00Z"
}
GET /api/v1/scan/{id}Retrieve scan results by ID.
प्रतिक्रिया:
{
"id": "scan-12345",
"status": "completed",
"results": [
{
"type": "vulnerability",
"severity": "high",
"description": "SQL injection detected"
}
]
}
We welcome contributions! Please follow these steps:
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)This project is licensed under the MIT License - see the LICENSE file for details.```bash python -m app --mode vulnerable --port 9101
कमिट किए गए टेलीमेट्री को पुनः उत्पन्न करें (पिन किया गया क्लॉक, रन के बीच बाइट-समान):```bash
python scripts/lab.py evidence
कार्य रनर, दोनों तरह से समान लक्ष्य:```bash make demo
| `--no-color` | आउटपुट में रंग अक्षम करें |
| `--debug` | डिबग मोड सक्षम करें |
| `--verbose` | वर्बोज़ आउटपुट सक्षम करें |
| `--quiet` | चुप्पी मोड सक्षम करें |
| `--version` | संस्करण जानकारी दिखाएं |
| `--help` | सहायता संदेश दिखाएं |```powershell
./make.ps1 demo
alice / Password123! → changed during the demo to Correct-Horse-Battery-9!
Fake, local, और इस repository में केवल यही credentials हैं।
---
## Security notes
**इस repository में जानबूझकर vulnerable code मौजूद है।**
`app/auth.py:_revoke_for_security_change` जब `AFTERLIFE_MODE=vulnerable` होता है तो जानबूझकर revoke करने में विफल रहता है।
Vulnerable path **default नहीं है**। `AFTERLIFE_MODE` का default `fixed` है, और `test_the_default_mode_is_the_safe_one` इसे pin करता है — ऐसा lab जिसका टूटा हुआ व्यवहार कुछ भी configure करना भूल जाने पर मिलता है, आखिरकार किसी असली चीज़ में copy हो जाएगा।
किसी भी mode में इसे network पर expose करना सुरक्षित नहीं है: bearer credentials response bodies में लौटाए जाते हैं, `GET /lab/credentials` किसी user की credential state के बारे में server का पूरा दृश्य dump कर देता है, `GET /lab/audit` design से unauthenticated है, `POST /security-change` स्वयं को `admin` grant कर लेगा, और कहीं भी TLS, rate limiting, CSRF protection या account lockout नहीं है। सब कुछ `127.0.0.1` पर bind होता है, और `python -m app` तथा `python -m console` दोनों किसी अन्य address को अस्वीकार करते हैं।
इस repository में कोई असली credential, key या secret मौजूद नहीं है। Signing key literal string `afterlife-lab-signing-key-not-a-secret-do-not-reuse` है; credential database का default `:memory:` है; telemetry उन values को बदल देती है जिनके key names credentials जैसे दिखते हैं और credentials को id तथा fingerprint से संदर्भित करती है। विवरण और reporting प्रक्रिया: **[SECURITY.md](https://github.com/het-p301204/afterlife/blob/main/SECURITY.md)**।
---
## Documentation
| | |
|---|---|
| [docs/detection.md](https://github.com/het-p301204/afterlife/blob/main/docs/detection.md) | rule pack: सभी तीन rule cards, state audit, आवश्यक telemetry, false positives, severity rationale, response runbooks |
| [docs/tradeoffs.md](https://github.com/het-p301204/afterlife/blob/main/docs/tradeoffs.md) | चार revocation architectures, JWT tension, और CVSS चर्चा |
| [docs/limitations.md](https://github.com/het-p301204/afterlife/blob/main/docs/limitations.md) | हर blind spot, fix क्या ठीक नहीं करता, audit क्या बंद करता है, और lab के अपने compromises |
| [docs/console-design.md](https://github.com/het-p301204/afterlife/blob/main/docs/console-design.md) | console mortality register क्यों है, palette और type निर्णय, और क्या हटाया गया |
| [docs/console-preview.html](https://github.com/het-p301204/afterlife/blob/main/docs/console-preview.html) | console, एक self-contained file में baked |
| [docs/figures/](https://github.com/het-p301204/afterlife/blob/main/docs/figures) | इस README के figures, payload से generated |
| [report/AFTERLIFE-report.md](https://github.com/het-p301204/afterlife/blob/main/report/AFTERLIFE-report.md) | vulnerable run के लिए एक generated incident report |
| [SECURITY.md](https://github.com/het-p301204/afterlife/blob/main/SECURITY.md) | local-only boundary, lab credentials, redaction contract |
| [evidence/](https://github.com/het-p301204/afterlife/blob/main/evidence) | sanitized sample telemetry, उससे उत्पन्न alerts, और state audit |
---
## Research references
2026-09-11 को primary sources के विरुद्ध verified। **VERIFIED** का अर्थ है कि vendor की स्वयं की advisory पढ़ी गई; **REPORTED** का अर्थ है कि विवरण vendor के बजाय किसी vulnerability database से आया है।
**Standards and classifications**
* [OWASP Top 10:2025 — A07 Authentication Failures](https://top10.owasp.org/2025/A07_2025-Authentication_Failures) — ठीक इसी विफलता का वर्णन करता है: ऐसे applications जो "correctly invalidate user sessions or authentication tokens" नहीं करते। इसकी CWE mapping में नीचे दिए गए तीनों शामिल हैं।
* [RFC 9700](https://datatracker.ietf.org/doc/rfc9700/) — *Best Current Practice for OAuth 2.0 Security* (BCP 240, January 2025)। इस lab के दो आधार: rotated refresh token का reuse चोरी का संकेत देता है (AFTERLIFE-002), और **revocation को पूरे token family को invalidate करना चाहिए, केवल वर्तमान token को नहीं** — जो AFTERLIFE-001 की thesis है, IETF practice के रूप में codified।
* [OWASP Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) — credential और security परिवर्तनों के बाद session invalidation
* [CWE-613: Insufficient Session Expiration](https://cwe.mitre.org/data/definitions/613.html) — *"permits an attacker to reuse old session credentials or session IDs for authorization"*। नीचे दिए गए तीनों CVEs इसी class में filed हैं।
* [CWE-287: Improper Authentication](https://cwe.mitre.org/data/definitions/287.html)
* [CWE-384: Session Fixation](https://cwe.mitre.org/data/definitions/384.html) — adjacent: एक session identifier जो authentication state परिवर्तन के बाद भी जीवित रहता है
**The pattern this lab reproduces**
* **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: Password Reset Does Not Revoke Existing Refresh Sessions*. `@strapi/admin` और `@strapi/plugin-users-permissions` ≤ 5.33.2; 5.33.3 में fixed। 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`. Published 2026-05-13.
Vendor advisory बताती है कि refresh-token invalidation step caller-supplied `deviceId` पर conditional था, और patch हर password change तथा reset पर सभी refresh tokens को invalidate करता है, चाहे `deviceId` दिया गया हो या नहीं। वही conditional है जिसे `_revoke_for_security_change` model करता है, और वही patch है जिसे fixed mode implement करता है।
Vendor context: [Strapi security disclosure, May 2026](https://strapi.io/blog/security-disclosure-of-vulnerabilities-cve-2025-64526-cve-2026-22599-cve-2026-22706-cve-2026-22707-and-cve-2026-27886).
**The same class, different mechanisms**
* **REPORTED** — [CVE-2026-1163](https://nvd.nist.gov/vuln/detail/CVE-2026-1163), parisneo/lollms. Password reset पर session invalidation *बिल्कुल नहीं*, 31-दिन के default session lifetime के साथ। इस bug का सरल संस्करण — और वही जिसे naive detector वास्तव में पकड़ लेगा।
* **REPORTED** — [CVE-2026-40934](https://nvd.nist.gov/vuln/detail/CVE-2026-40934), Jupyter Server ≤ 2.17.0 (fixed 2.18.0)। Cookie-signing secret एक static file में persist किया जाता है और password change पर कभी rotate नहीं होता, इसलिए reset से पहले जारी किए गए cookies उसके पार cryptographically valid बने रहते हैं। इसी परिणाम तक पहुँचने का तीसरा रास्ता: यहाँ *credential* इसलिए valid है क्योंकि *key* कभी नहीं बदली, जो [docs/tradeoffs.md](https://github.com/het-p301204/afterlife/blob/main/docs/tradeoffs.md) में Option D का failure mode है।
तीन products, तीन mechanisms — एक conditional parameter, एक missing call, एक unrotated key — एक परिणाम: reset सफल हुआ और attacker बना रहा।
---
<div align="center">
**The attacker is not detected because their request looks malicious.**
उनकी हर request well-formed थी, सही ढंग से signed थी, और एक ऐसा credential लेकर आई थी जो server ने अभी उनके लिए mint किया था।
**They are detected because a credential lineage that should have died at a security event was accepted after it.**
<br>
<sub>MIT · a local lab · nothing here is safe to deploy</sub>
</div>
| नियम | गंभीरता | उत्तर देता है | सक्रिय होता है |
|---|
| AFTERLIFE-002 | रीफ्रेश क्रेडेंशियल का पुनः उपयोग | CRITICAL / MEDIUM | क्या यह चोरी हुआ था? | पुनः उपयोग के समय |
| AFTERLIFE-003 | सुरक्षा परिवर्तन पर अपूर्ण निरस्तीकरण | HIGH / LOW | क्या कंटेनमेंट चला? | परिवर्तन के समय — किसी हमलावर की आवश्यकता नहीं |
| AFTERLIFE-001 | निरस्तीकरण-पश्चात क्रेडेंशियल वंशावली का उपयोग | HIGH | क्या बासी वंशावली का उपयोग किया गया? | पहली स्वीकृति के समय |
| RULE PACK |
-h--help-V--version| परिदृश्य | मोड | घटनाएँ | सबसे खराब | नियम सक्रिय | stale | dormant |
|---|
legitimate-only | vulnerable | 19 | LOW | AFTERLIFE-003 | 0 | 0 |
legitimate-only | fixed | 19 | – | कोई नहीं | 0 | 0 |
stolen-refresh | vulnerable | 22 | HIGH | AFTERLIFE-001, AFTERLIFE-003 | 1 | 0 |
stolen-refresh | fixed | 23 | – | कोई नहीं | 0 | 0 |
stolen-refresh-with-device-id | vulnerable | 19 | LOW | AFTERLIFE-003 | 0 | 0 |
stolen-refresh-with-device-id | fixed | 19 | – | कोई नहीं | 0 | 0 |
multi-device | vulnerable | 29 | HIGH | AFTERLIFE-001, AFTERLIFE-003 | 2 | 0 |
multi-device | fixed | 29 | – | कोई नहीं | 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 | – | कोई नहीं | 0 | 0 |
mfa-change | vulnerable | 18 | HIGH | AFTERLIFE-001, AFTERLIFE-003 | 1 | 0 |
mfa-change | fixed | 18 | – | कोई नहीं | 0 | 0 |
account-recovery | vulnerable | 18 | HIGH | AFTERLIFE-001, AFTERLIFE-003 | 1 | 0 |
account-recovery | fixed | 18 | – | कोई नहीं | 0 | 0 |
expired-lineage | vulnerable | 17 | LOW | AFTERLIFE-003 | 0 | 0 |
expired-lineage | fixed | 20 | – | कोई नहीं | 0 | 0 |
| मामला | परिणाम | क्यों |
|---|
| पासवर्ड परिवर्तन, फिर प्रतिस्थापन सत्र का उपयोग | कोई अलर्ट नहीं | नई lineage का रूट वॉटरमार्क पर या उसके बाद है |
रीसेट फिर तत्काल ब्राउज़िंग (/me, /profile, /settings) | कोई अलर्ट नहीं | एक ताज़ा lineage, एक ताज़ा रूट |
| फ़ोन, लैपटॉप और टैबलेट, सभी सही ढंग से रोटेटेड | कोई अलर्ट नहीं | प्रत्येक लॉगिन अपनी स्वयं की lineage है |
| एक डिवाइस जिसने परिवर्तन के बाद लॉगिन किया, हमलावर के साथ ब्राउज़ कर रहा है | कोई अलर्ट नहीं | ताज़ा रूट — और stale lineage अभी भी अकेले अलर्ट करती है |
| एक stale क्रेडेंशियल जिसे अस्वीकार किया गया | कोई अलर्ट नहीं | result: failure को बाहर रखा गया है; यह बचाव के लिए साक्ष्य है |
| घटकों के बीच 2s तक क्लॉक स्क्यू | कोई अलर्ट नहीं | प्रलेखित सहनशीलता |
| एक विफल पासवर्ड परिवर्तन | कोई अलर्ट नहीं | एक एंकर नहीं |
| परिवर्तन से पहले की गतिविधि | कोई अलर्ट नहीं | समय-क्रम जाँच |
एक रोटेटेड क्रेडेंशियल (reason: rotated) | मृत्यु के रूप में नहीं गिना जाता | एक क्रेडेंशियल खर्च हुआ, एक lineage मारी नहीं गई |
| 40 दिन पुराना सत्र जिसके क्रेडेंशियल समाप्त हो गए | एक survivor नहीं | समाप्ति प्रति lineage ट्रैक की जाती है |
| एक बिल्कुल नए खाते पर पहला पासवर्ड परिवर्तन | कोई अलर्ट नहीं | अंदर जाते समय कुछ भी जीवित नहीं था |
| एक stale lineage से Refresh-token रोटेशन | ALERT | ताज़ा टाइमस्टैम्प, stale वंशावली — यही finding है |
| परिवर्तन | क्रेडेंशियल्स अविश्वसनीय क्यों हो जाते हैं |
|---|
| पासवर्ड परिवर्तन / रीसेट | वह रहस्य जिसके साथ सत्र स्थापित हुआ था, चला गया |
| MFA नामांकन या परिवर्तन | जिन कारकों के साथ सत्र स्थापित हुआ था, वे खाते के कारक नहीं हैं |
| भूमिका परिवर्तन / विशेषाधिकार वृद्धि | क्रेडेंशियल एक भिन्न प्राधिकरण के अंतर्गत बनाया गया था |
| खाता पुनर्प्राप्ति | निर्माण से ही, खाता एक क्षण पहले किसी और के हाथ में हो सकता है |
| लॉगिन | अन्य को नहीं निरसित करता — एक नई वंशावली, यह कथन नहीं कि पुराने अविश्वसनीय हैं |
| तंत्र | क्या देता है | क्या खर्च होता है |
|---|
| A | प्रति-उपयोगकर्ता निरसन वॉटरमार्क | एक लेखन सब कुछ निरसित कर देता है, जिसमें वे क्रेडेंशियल्स भी शामिल हैं जिन्हें सर्वर भूल गया है; O(1) भंडारण और जाँच | रीड पथ पर सर्वर-साइड स्थिति; टाइमस्टैम्प अर्थ ठीक-ठीक सही होना चाहिए; वॉटरमार्क के बाद जारी किए गए क्रेडेंशियल्स के बारे में कुछ नहीं कहता |
| B | अल्पकालिक एक्सेस + निरसनीय रीफ्रेश | रीड-पथ स्थिति के बिना एक्सेस-टोकन क्षति को सीमित करता है | चोरी हुआ एक्सेस टोकन समाप्त होने तक वैध रहता है; रीफ्रेश पक्ष को अभी भी स्थिति चाहिए — यही वह पैटर्न है जिसके भीतर CVE रहता है |
| C | स्पष्ट डिनाइलिस्ट | सटीक; घटना प्रतिक्रियाकर्ता के लिए सुपाठ्य; वह टेलीमेट्री उत्पन्न करता है जो नियंत्रण साबित करती है | स्थिति बढ़ती है और सफ़ाई चाहिए; केवल उसे निरसित करता है जिसे आपने गिनना याद रखा — वही क्वेरी जिसे बग ने गलत किया |
| D | पूर्णतः स्टेटलेस JWT | रीड-पथ स्थिति बिल्कुल नहीं | कोई निरसन नहीं है। TTL और कुंजी रोटेशन ही एकमात्र उपाय हैं |
| Variable | Description |
|---|
KITPLOIT_SERVER | Server URL |
KITPLOIT_TOKEN | API token |
KITPLOIT_FORMAT | Default output format |
KITPLOIT_VERBOSE | Enable verbose mode (true/false) |
| Code | Meaning |
|---|
0 | Success |
1 | General error |
2 | Invalid arguments |
3 | Authentication failure |
4 | Resource not found |
5 | Server error |
| Endpoint | Method | Description |
|---|
/api/v1/scans | GET | List all scans |
/api/v1/scans | POST | Create a new scan |
/api/v1/scans/{id} | GET | Get scan details |
/api/v1/scans/{id} | DELETE | Delete a scan |
/api/v1/scans/{id}/results | GET | Get scan results |
/api/v1/targets | GET | List all targets |
/api/v1/targets | POST | Add a new target |
| Code | Meaning |
|---|
0 | Success |
1 | General error |
2 | Invalid arguments |
3 | Authentication failure |
4 | Network error |
5 | Scan timeout |
-h--help| Code | Meaning |
|---|
0 | Success |
1 | General error |
2 | Invalid arguments |
3 | Connection failed |
4 | Authentication failed |
-h--help