Co to je. Očištěný výřez z interního auditu vlastního kódu — vlastní kód jsme prohnali svojí útočně vedenou metodou („dogfooding"). Identifikátory cíle jsou redigované; metoda, nálezy, závažnosti a trasy dat jsou doslovné. Je to reprezentativní ukázka klientské dodávky, ne marketingová maketa.
| Cíl | interní API služba na loopbacku (název redigován) — lokální asynchronní brána na úkoly |
| Režim | JEN PRO ČTENÍ — testy zneužití byly napsané, ale nespuštěné; každý nález nese statickou trasu vstup→místo užití s konkrétními payloady. Žádný zdrojový soubor nebyl změněn. |
| Metoda | Recon → Paralelní lov → Adverzní falsifikace → Filtr proveditelnosti → nezávislé ověření jen pro čtení (kdo hodnotí ≠ kdo našel) |
| Nálezy | 0 Critical, 0 High, 1 Medium, 7 Low — všechny Confirmed (další 2 kandidáti zabiti při falsifikaci) |
Většina skenerů optimalizuje na úplnost — vynesou všechno, co se shoduje se vzorem slabiny, a třídění nechají na vás. My optimalizujeme na přesnost pod protivníkem: každý kandidát musí přežít krok, jehož jediným úkolem je vyvrátit ho.
Recon ─▶ Paralelní lov ─▶ Adverzní falsifikace ─▶ Filtr proveditelnosti ─▶ Nezávislé ověření
mapuj mnoho hypotéz „dokaž, že to NENÍ „dosáhne na to jen pro čtení,
vstupy současně skutečné" (výchozí skutečný aktér?" jiný kontext
a místa stav: vyvráceno) (hodnotí ≠ našel)
užití
Při tomhle běhu falsifikační krok zabil 2 z 10 kandidátů jako plané poplachy (viz Zabití kandidáti níže). Zpráva, ve které je jen ověřené, má větší cenu než zpráva dlouhá: rozpočet na opravy padne na skutečné vady, ne na jejich třídění.
Služba naslouchá jen na 127.0.0.1 (gateway.py:475) a každou cestu kromě /health
ověřuje jedním sdíleným instalačním tokenem X-Api-Token
(secrets.token_urlsafe(24), porovnání v konstantním čase :273). Existuje přesně
jeden principál — kterýkoli lokální proces, který ten token drží — takže žádná
hranice mezi nájemníky neexistuje. To stropuje každý nález: „číst/zapsat libovolný
úkol" je ověřený základní stav a dosah je jeden loopbackový počítač. Nejcennější nález
(F-001) je nezávislý na principálovi (podvržení autorství + destrukce), proto
Medium; všechno ostatní je Low. Poctivé vymezení poráží nafukování závažnosti.
| ID | Nález | CWE | Severity | Status |
|---|---|---|---|---|
| F-001 | Podvržení odpovědi + vynucené smazání libovolného čekajícího úkolu | CWE-290 / CWE-639 | Medium | Confirmed |
| F-002 | next_id() souběh TOCTOU → duplicitní id / přepis kontextu |
CWE-367 / CWE-362 | Low | Confirmed |
| F-003 | Neomezené čtení těla požadavku (/submit, /upload) |
CWE-400 | Low | Confirmed |
| F-004 | Neomezené dekódování base64 přílohy + zápis na disk (/upload) |
CWE-400 | Low | Confirmed |
| F-005 | Neomezené čtení těla požadavku (/answer) |
CWE-400 | Low | Confirmed |
| F-006 | Sdílený rozpočet globálního omezovače → odepření služby | CWE-770 / CWE-307 | Low | Confirmed |
| F-007 | Neověřený /health spouští úklid souborů |
CWE-306 | Low | Confirmed |
| F-008 | Ověřovací token v čitelném souboru a na výstupu; chmod je na Windows bez účinku |
CWE-312 | Low | Confirmed |
Ke každému nálezu byl dodán statický důkaz a test zneužití (napsaný, nespuštěný).
Sloupce Severity a Status zůstávají v angličtině schválně — jsou to kanonické tokeny, ze kterých se počítá pečeť na konci dokumentu. Kdyby se přeložily, česká a anglická verze by daly jiný otisk a recept by nešel splnit.
| Pole | Hodnota |
|---|---|
| CWE | CWE-290 (obejití ověření podvržením) / CWE-639 (obejití autorizace přes klíč řízený uživatelem) |
| Severity | Medium |
| Vstupní bod | POST /answer |
| Místo | gateway.py:448-451 (zápis); čtení při dotazování :302-308; smazání při úklidu :186-191 |
| Trasa dat | útočník nastaví reply_to na libovolné id úkolu → obsluha připojí {author:"worker", …} bez kontroly, že úkol existuje a je otevřený (:448-451) → (a) dotazující se rozhraní dostane podvrženou odpověď jako skutečný výstup pracovníka (:304-307); (b) úklid po „zodpovězení" smaže přechodný soubor s plným kontextem cílového úkolu dřív, než ho skutečný pracovník přečte (:186-191). |
| Oprava | V obsluze odpovědi ověřit, že cílový úkol existuje a je otevřený, teprve pak připojovat; smazání po zodpovězení navázat na ověřenou odpověď; při čtení vynucovat původ odpovědi. |
| Kořenová příčina | Klíč reply_to řízený uživatelem není navázaný na existenci a otevřený stav; pole autora je napevno. |
| Proč Medium (a ne Low) | Nezávislé na principálovi: podvrhnout a zničit rozpracované úkoly je nad rámec ověřeného základního stavu „posílat odpovědi". |
Oprava — nasazena a přeměřena týž den (identifikátory redigované, logika doslovná):
# obsluha POST /answer
- # kdokoli mohl odpovědět na JAKÉKOLI id — existence nekontrolovaná, autor napevno
- append_reply(author="worker", reply_to=req["reply_to"], text=req["text"])
+ # odpověď musí odpovídat skutečnému, stále OTEVŘENÉMU požadavku — vše ostatní se odmítá
+ if read_request(reply_to) is None: # takový otevřený požadavek není -> 404
+ return 404
+ if already_answered(reply_to): # dvakrát odpovědět nelze -> 409
+ return 409
+ append_reply(author="worker", reply_to=reply_to, text=text)
Přeměření po opravě: cesta podvržení teď vrací 404 a nepřipojí nic; čekající požadavek přežije úklidový průchod (útočník ho už nemůže vynuceně převést do množiny „zodpovězených"). Obě větve — podvržení autorství i vynucené smazání — se zavírají u téže pojistky. Doba od potvrzení k ověřené opravě: týž den.
next_id() souběh TOCTOU| Pole | Hodnota |
|---|---|
| CWE | CWE-367 (TOCTOU) / CWE-362 (souběh) |
| Severity | Low |
| Vstupní bod | POST /submit, POST /upload (souběžnost) |
| Místo | gateway.py:136-138 (next_id), volající :336, :390 |
| Trasa dat | next_id() odvozuje id z len(read_events()) (:137) → pod vláknovým HTTP serverem (:475) přečtou dva souběžné požadavky stejný počet dřív, než kterýkoli z nich připojí → oba přidělí totéž id → duplicitní záznam a přepsaný přechodný kontext jednoho z úkolů. Zámek omezovače hlídá jen počitadlo, ne přidělování id. |
| Oprava | Serializovat next_id()→připojení→zápis pod jedním vyhrazeným zámkem, nebo odvozovat id z monotónního počitadla či UUID místo z délky záznamu. |
/submit, /upload)| Pole | Hodnota |
|---|---|
| CWE | CWE-400 (neřízená spotřeba zdrojů) |
| Severity | Low |
| Vstupní bod | POST /submit, POST /upload |
| Místo | gateway.py:323, :376 |
| Trasa dat | útočník nastaví velké Content-Length → rfile.read(n) bez stropu MAX_BODY → jediná neomezená alokace. Omezovač stropuje počet požadavků, ne jejich velikost. |
| Oprava | Zavést MAX_BODY na úrovni modulu; požadavky nad limit, bez hlavičky nebo se zápornou hodnotou odmítat kódem 413/400 v jedné sdílené pomocné funkci _read_json_body(), kterou použije každá POST cesta. |
/upload)| Pole | Hodnota |
|---|---|
| CWE | CWE-400 |
| Severity | Low |
| Vstupní bod | POST /upload |
| Místo | gateway.py:392-403 |
| Trasa dat | útočník pošle velkou base64 přílohu → b64decode (:394) → open(path,"wb").write(raw) (:402-403) bez kontroly délky → na disk se zapíšou bajty řízené útočníkem; úklid podle TTL je pomalejší než rychlost zápisu. |
| Oprava | Vynutit MAX_ATTACHMENT_BYTES na dekódované délce ještě před zápisem na disk; nadměrné odmítnout. |
/answer)| Pole | Hodnota |
|---|---|
| CWE | CWE-400 |
| Severity | Low |
| Vstupní bod | POST /answer |
| Místo | gateway.py:445 |
| Trasa dat | tentýž vzorec jako F-003 u třetí obsluhy: int(Content-Length) → rfile.read(n) bez stropu. |
| Oprava | Tentýž sdílený strop _read_json_body() jako u F-003 — opraví všechny tři cesty u kořene. |
| Pole | Hodnota |
|---|---|
| CWE | CWE-770 (alokace bez limitu) / CWE-307 (nedostatečné omezení opakovaných pokusů) |
| Severity | Low |
| Vstupní bod | všechny cesty (brána běží před ověřením) |
| Místo | gateway.py:261-269, kontrola :289, :312 |
| Trasa dat | omezovač drží jeden globální seznam zásahů bez klíče na volajícího a kontroluje se před ověřením → neověřený lokální volající, který vyčerpá okno, vynutí 429 pro oprávněného operátora (sdílený rozpočet). |
| Oprava | Klíčovat omezovač podle volajícího (adresa klienta a/nebo token) s odděleným oknem pro každý klíč. |
/health spouští úklid souborů| Pole | Hodnota |
|---|---|
| CWE | CWE-306 (chybějící ověření u kritické funkce) |
| Severity | Low |
| Vstupní bod | GET /health |
| Místo | gateway.py:290-292 → úklid :161-192 |
| Trasa dat | větev /health spouští úklid souborů před kontrolou ověření (:293) → kterýkoli lokální neověřený volající vyvolá smazání přechodných souborů, které už jsou k úklidu způsobilé (zodpovězené / prošlé TTL). Jen příležitostné, ale skutečné — a řetězí se s F-001 (neověřené smazání úkolů označených útočníkem). |
| Oprava | Udělat z /health čisté čtení — vypustit volání úklidu; spolehnout se na ověřený průchod a na úklid při startu. |
| Pole | Hodnota |
|---|---|
| CWE | CWE-312 (ukládání citlivé informace v čitelné podobě) |
| Severity | Low |
| Místo | gateway.py:66-70 (soubor), :468 (výstup) |
| Trasa dat | token_urlsafe(24) se zapisuje v čitelné podobě do .api-token; os.chmod(...,0o600) je na Windows bez účinku (:70) → soubor může zůstat čitelný pro všechny; celý token se navíc vypisuje na standardní výstup (:468) → lokální uživatel, který není operátor, si přečte soubor nebo konzoli a získá celý token (a ten odemyká F-001/006/007). |
| Oprava | Šifrovat v klidu (klíčenka OS / DPAPI); nastavit skutečné ACL pro Windows místo spoléhání na POSIXový chmod; výpis na konzoli maskovat (otisk / poslední čtyři znaky). |
Seznam nálezů je důvěryhodný jen tehdy, když doloží i to, co bylo prověřeno a čisté. Každý zvenku řízený vstup má přiřazenou dispozici (názvy polí redigované):
| Vstup | Dispozice |
|---|---|
hlavička Origin |
BEZPEČNÉ — řídí jen odraz CORS, ne přístup; zvláštnost s prefixem nejde využít (token je pořád vyžadován). |
X-Api-Token |
BEZPEČNÉ (ověření) — porovnání v konstantním čase. Uložení tokenu je CWE-312 → F-008. |
Content-Length |
KANDIDÁT → F-003 (/submit,/upload), F-005 (/answer). |
hlavní payload |
BEZPEČNÉ — před uložením redigováno a zbaveno tajemství; žádné místo užití pro injektáž. |
objekt metadata (různá textová pole) |
BEZPEČNÉ — zabalený/redigovaný kontext; žádné místo užití. |
pole url pro klasifikaci |
BEZPEČNÉ — slouží jen ke klasifikaci (povolení jako výchozí stav je zaznamenáno jako kód se zápachem → viz obrana do hloubky). |
přepínač option |
BEZPEČNÉ — není místem užití. |
base64 attachment |
KANDIDÁT → F-004. |
klíč reply_to |
KANDIDÁT → F-001. |
tělo odpovědi text |
BEZPEČNÉ — zneužití jde přes navázání reply_to, ne přes obsah textu. |
id v cestě {id} |
BEZPEČNÉ — čistička re.sub(r"[^a-z0-9\-]","",id) blokuje průchod adresářem; IDOR mezi principály není vyjádřitelný (jeden sdílený token). |
| čtení záznamů druhého řádu | BEZPEČNÉ — vlastní zápisy; přetržený řádek se při čtení sám ošetří. |
| přepínače příkazové řádky | ZÁMĚR NÁVRHU — jen lokální operátor; není to místo užití dat. |
Všechny vstupy vyřešeny. Nálezy, které nejdou navázat na jediný vstup (souběžnost,
globální omezovač, /health bez vstupu), jsou zachyceny jako F-002 / F-006 / F-007.
eval / exec / subprocess / pickle / yaml.load / os.system v produkčních cestách./health vynucuje porovnání tokenu v konstantním čase; větev, která by ověření přeskočila, neexistuje.| Kořenová příčina | Nalezeno výskytů | Ošetřeno (=nález) | Zbývá |
|---|---|---|---|
Neomezené rfile.read(Content-Length) |
3 | 3 (F-003 ×2, F-005) | 0 |
Neomezené b64decode → na disk |
1 | 1 (F-004) | 0 |
next_id() TOCTOU bez zámku |
3 | 3 (F-002) | 0 |
| Rozpočet globálního omezovače | 1 | 1 (F-006) | 0 |
| Cesta měnící stav před ověřením | 6 cest | 5 ověřených + 1 (F-007) | 0 |
| Tajemství v klidu / na výstupu | 3 | 1 (F-008) + 1 vyloučeno (jen vývoj) | 0 |
| Místa pro injektáž | 0 | 0 | 0 |
| Průchod adresářem | 2 | 2 (ošetřeno čističkou) | 0 |
Každá kořenová příčina se mapuje 1:1 na nálezy — žádný osiřelý výskyt.
| Vzorec | Proč to dnes není nález | Riziko, když se podmínky změní | Doporučení |
|---|---|---|---|
| Klasifikátor obsahu používá blokační seznam s povolením jako výchozím stavem | nedosáhne na žádné místo užití řízené útočníkem — je to mezera v úplnosti politiky, ne injektáž | citlivý web, jehož URL neobsahuje uvedené klíčové slovo, pošle své tělo ven | přejít na seznam povolených / klasifikaci podle obsahu; ukotvovat jednotně |
Kontrola Origin přes shodu prefixu startswith(...) |
jen odráží hlavičku CORS; přístup pořád hlídá token | kdyby se odraz někdy použil jako rozhodnutí o přístupu, podobný původ by prošel | používat přesnou shodu původu / rozebrat hostitele |
| Externí modul redakce selhává povolením (při chybě vrátí text beze změny) | mimo záběr skenu; není řízen útočníkem | když modul chybí nebo je rozbitý, redakce tiše degraduje na průchod bez zásahu | selhávat zákazem (zadržet), když se modul nepodaří načíst; degradaci zapsat do logu |
Dvě hypotézy došly do adverzního kroku a byly vyvráceny — uvádíme je schválně, protože co skener zahodí je stejně vypovídající jako to, co si nechá:
Těsně vymezená lokální služba s jedinou skutečnou vadou návrhu (F-001) a hroznem běžných mezer ve zpevnění — 1 Medium, 7 Low, nulové nafukování a dva plané poplachy chycené dřív, než se dostaly do zprávy. Takhle vypadá ověřená dodávka.
Redigovaná ukázka · útočně vedený rozbor · režim jen pro čtení · kdo hodnotí ≠ kdo psal.
Zpráva výše je snímek běhu z 2026-07-19 a nepřepisuje se. Tohle je dodatek: co se změnilo, kdy, a jak se ta změna přeměřila. Ověřoval ji průchod psaný proti každé opravě — kdo hodnotí ≠ kdo psal platí i na opravu.
| ID | Zavřeno | Co se změnilo | Jak se to přeměřilo |
|---|---|---|---|
| F-001 | 2026-07-19 | Klíč odpovědi navázán na existující, stále otevřený požadavek; dvojí odpověď se odmítá. | Přeměřeno týž den: cesta podvržení vrací 404 a nepřipojí nic; čekající požadavek přežije úklidový průchod. |
| F-002 | 2026-08-28 | Přidělování id serializováno pod zámkem s procesní značkou nejvyššího dosaženého čísla. | 40 souběžných přidělení dalo 40 různých id. |
| F-003 | 2026-08-28 | Jedna sdílená čtečka těla se stropem pro všechny POST cesty — chybějící, záporné i nadměrné Content-Length se odmítá ještě před alokací. | Deklarovaných 99 MB → 413; záporná hodnota → 400; chybějící hlavička → 400. |
| F-004 | 2026-08-28 | Strop vynucen na DEKÓDOVANÉ délce přílohy, ještě před zápisem na disk. | Nadměrný obrázek → 413 a na disk se nezapsal žádný soubor. |
| F-005 | 2026-08-28 | Táž sdílená čtečka — oprava šla ke kořeni, ne třikrát do tří obsluh. | Deklarovaných 99 MB na třetí cestě → 413. |
| F-006 | 2026-08-28 | Omezovač klíčovaný podle třídy volajícího místo jednoho globálního seznamu. | Neověřené zaplavení už nedusí ověřeného volajícího (200); po návratu k jedinému globálnímu kbelíku se 429 znovu objeví. |
| F-007 | 2026-08-28 | Cesta na kontrolu života je čisté čtení; úklid běží při startu a jen na ověřené cestě. | Přechodný soubor způsobilý k úklidu přežije neověřené volání a výslovný úklid ho pořád smaže. |
| F-008 | 2026-08-28 | Přihlašovací údaj v konzoli maskován (vypsat ho jde jen na vyžádání); místo POSIXového volání, které na Windows nic nedělá, se nastavuje skutečné ACL. | Kontrola zdroje plus volání ACL na souboru s tokenem. |
Jeden řádek na každý nález z tabulky Souhrn — ID|CWE|Severity|Status, přesně jak je vytištěno. Jeden řádek na každý řádek tabulky Náprava — ID|fixed|datum. Jeden řádek run|2026-07-19. Všechny řádky seřadit abecedně dohromady, spojit znakem nového řádku (bez koncového), SHA-256. Všechny vstupy jsou vytištěné na této stránce.
Nahrazuje 414329d8ff0b6e68a173a6d4d1383fba… — dřívější pečeť jen nad nálezovou sadou, než dodatek o nápravě existoval. Dokument, který se změnil, to má říct řetězem, ne potichu.
Co tahle pečeť NEDOKAZUJE. Nedosvědčuje prózu, opravy ani to, kdy stránka vyšla, a není časovou autoritou. Zamyká nálezovou sadu — identifikátory, CWE, závažnosti a dispozice — aby pozdější verze téhle stránky nemohla potichu nález přidat, ztratit nebo mu snížit závažnost, aniž se změní otisk. To je celé tvrzení. Otisk je shodný s anglickou verzí — pečetí se nálezová sada, ne text.