diff, který nikdo nečte
Z jednoho klientského auditu minulý kvartál mi utkvěl jeden údaj, a sedí i s obecnějšími čísly, co se povídají: zhruba 41 % backendového kódu generovaného AI jde do produkce s příliš širokými oprávněními a asi 60 % vývojářů scope nikdy nezúží, než kód pustí ven. Když si ta dvě čísla položíte vedle sebe, popisují konkrétní způsob selhání. Access control model celé codebase se přetváří funkci po funkci podle toho, co model náhodou napovídal ve čtyři odpoledne ve čtvrtek, a nikdo se na to pořádně nepodívá podruhé.
Důvod je nudný a lidský. Když člověk píše IAM policy od nuly, cítí tření. Musí přemýšlet, co ta služba vlastně potřebuje, jde si dohledat přesný název akce, naštve se, a ta naštvanost je přirozená pojistka proti přehnaným právům. LLM žádné tření necítí. Řeknete mu, ať vám zapojí upload do S3, a naservíruje vám policy s s3:* na Resource: "*", protože to funguje vždycky, v demu nikdy nevyhodí permissions error, a vývojář, který PR jen prolétne, vidí blok policy, který vypadá strukturálně správně, a schválí ho.
Ta správnost je past. JSON je validní, Terraform naplánuje čistě, testy projdou, protože testy taky vygeneroval ten samý model, který si přidělil admina. Nic není rozbité tak, jak vaše CI umí poznat rozbité. Blast radius se jen potichu zvětšil a diff, který za to může, vypadal jako každý jiný diff.
Ve steezru tohle na reálných projektech uklízíme zhruba poslední rok a ten vzorec se opakuje natolik konzistentně, že si zaslouží pořádný playbook, ne jen dojmy.
co si nejdřív vygrepovat
Začněte lacinými výhrami, těmi, co najdete ripgrepem za nějakých devadesát vteřin, ještě než začnete stavět něco složitějšího. Wildcard IAM policy jsou ta nejzjevnější. Pusťte rg '"Action":\s*"[a-z0-9]+:\*"' přes Terraform a všechny inline policy JSONy a zvlášť si vygrepněte "Resource": "*", protože kombinace wildcardu na akci služby a wildcardu na resource je místo, kde vzniká skutečná škoda. Narazíte třeba na Lambdu, která volá jen dynamodb:GetItem, ale sedí na policy s dynamodb:* na každé tabulce v účtu.
V Djangu je ekvivalentní smrad chybějící row-level scoping. Model má foreign key user, view natáhne objekty a queryset je Model.objects.all() bez filtru, který by výsledky svázal zpátky s request.user. LLM, které píše DRF viewset, vám s klidem dá queryset = Invoice.objects.all() a permission class IsAuthenticated, což znamená, že libovolný přihlášený uživatel si vytáhne jakoukoli fakturu jen inkrementováním ID. Grepněte .objects.all() uvnitř views a viewsetů, každý zásah považujte za vinný, dokud se neprokáže scopování, a ověřte, jestli get_queryset skutečně zužuje podle žádajícího uživatele.
Pokud jedete na Postgresu a spoléháte na RLS, vygrepněte tabulky, které mají ENABLE ROW LEVEL SECURITY bez odpovídajícího CREATE POLICY. Zapnout RLS bez policy na tabulce, kterou používá role, jež není owner, je footgun, který modely rády implementují jen napůl. Napíšou enable řádek, zapomenou na policy, a teď je buď všechno zakázané, nebo, pokud není nastaven FORCE a aplikace se připojuje jako owner tabulky, se RLS potichu úplně obchází.
U Go backendů lovte overprivileged service account. Podívejte se, jak se služba autentizuje do cloudu, a zkontrolujte, jestli na všechny operace nepoužívá jeden tučný credential. Pořád nacházíme jediný GCP service account s roles/editor, který dělá práci za to, co měly být čtyři úzce scopnuté účty, protože setup skript vygenerovaný AI si požádal o editor, jelikož to je role, se kterou quickstart naběhne napoprvé.
proč tohle vaše SAST nástroje nechytí
Nepříjemná pravda je, že Semgrep, Snyk Code, CodeQL a zbytek statické analýzy, kterou už platíte, byly postavené na hledání jiné kategorie chyb. Jsou vyladěné na injection, na hardcoded secrets, na známé zranitelné závislosti, na taint-flow problémy, kde se nedůvěryhodný vstup dostane do nebezpečného sinku. Kolem toho se stavěla trénovací data i knihovny pravidel a chytají to dobře.
Příliš široká IAM policy ale není chyba v tomhle smyslu. Je to validní, dobře zformovaná, záměrně vypadající konfigurace, která jen náhodou uděluje víc, než aplikace potřebuje. Žádný tainted input, žádný nebezpečný sink, žádné CVE. Policy s s3:* je pro taint-flow engine k nerozeznání od policy, která uděluje přesně ty správné tři akce, protože obojí jsou jen stringy v konfiguračním souboru. Nástroj nemá žádný model toho, co tahle konkrétní služba smí dělat, takže nemá s čím porovnávat.
Chybějící row-level filtr v Djangu je pro scanner ještě horší, protože kód je správně. Invoice.objects.all() je naprosto legální queryset. Zranitelnost existuje jen v mezeře mezi tím, co kód dovolí, a tím, co zamýšlel váš autorizační model, a ten záměr žije ve vaší hlavě nebo v threat model dokumentu, ne v AST. Tohle je autorizační logika, což byla odjakživa třída chyb, na kterou jsou automatické nástroje nejhorší, a už dřív to bylo první položkou na seznamu OWASP, ještě než každý vývojář dostal autocomplete, který ji generuje jako výchozí stav.
Takže se z tohohle nedá vykoupit nástroji, které máte. Potřebujete pravidla, která si napíšete sami a která kódují vaše vlastní očekávání least-privilege, a potřebujete je pouštět na každém PR.
lint krok na oprávnění v CI
Dobrá zpráva je, že většina toho, co jsem popsal výš, je grep, a grep se do CI dostane čistě. Stavíme dedikovaný permissions job, který běží odděleně od hlavního lint stage, takže je v PR checkech vizuálně odlišný a nezapadne pod sto varování z prettieru.
Semgrep je správné místo pro strukturovaná pravidla, i když jeho výchozí rulesety tohle nechytnou, protože zrovna na psaní vlastních pravidel se vyplatí. Pravidlo, které označí jakýkoli AWS policy statement kombinující wildcard na akci s wildcardem na resource, je zhruba osm řádků YAMLu, a pravidlo, které označí DRF viewsety, jejichž queryset je holé .all() bez přepsaného get_queryset, tak patnáct. Na Postgres RLS mezeru pouštíme malý skript proti schématu, který vypíše každou tabulku se zapnutým RLS a porovná ji s tabulkami, co mají aspoň jednu policy, a build shodí na jakémkoli nesouladu.
Na straně cloudového IAM mají nástroje jako checkov a tfsec (teď pod Trivy) nějaké vestavěné kontroly šíře policy, tak je zapněte, ale ber jejich výstup jako spodní hranici a navrch přidejte vlastní OPA/conftest policy kódující vaše skutečné služby. Něco jako deny, když service account odkazuje na roles/editor nebo roles/owner, pokud není na explicitním allowlistu s komentářem, proč.
Ať ten job padá natvrdo, ne jen varuje. Varování v CI je věc, kterou se všichni do týdne naučí přeskrolovat. Když se objeví wildcard policy, měla by zablokovat merge a donutit člověka buď scope opravit, nebo přidat inline výjimku s odůvodněním, a ta výjimka by se sama měla objevit v diffu, aby prošla review. Celý smysl je vrátit tření, které LLM odstranilo, přesně ve chvíli, kdy se kód snaží dostat do hlavní větve.
návyk při review, který opravdu vydrží
Nástroje vás dostanou většinu cesty, ale trvalá oprava je změnit, na co se reviewer dívá, když se diff dotkne čehokoli auth povahy. Začali jsme každé PR, které přidává IAM policy, permission class, middleware nebo nový service credential, brát jako review, kde druhý inženýr musí nahlas v PR threadu odpovědět na jednu otázku: jaká je nejmenší množina oprávnění, kterou tohle potřebuje, a odpovídá jí kód?
Zní to těžkopádně, ale není, protože u většiny PR je odpověď třicet vteřin psaní. Hodnota je v tom, že to reviewera donutí policy skutečně přečíst, ne jen pattern-matchovat na její tvar, což je přesně ten způsob selhání, který přehnaná práva od AI propustil úplně poprvé. Když někoho donutíte pojmenovat zamýšlený scope, nesoulad mezi záměrem a kódem okamžitě vyskočí a chytnete to dynamodb:*, co mělo být tři read akce.
Taky si na daném projektu vedeme průběžnou poznámku o konkrétních vzorcích přehnaných práv, které modely mají tendenci chrlit, protože se shlukují. Na jedné Go pipeline na zpracování dokumentů, kterou jsme stavěli, asistent pořád sahal po jediném storage-admin credentialu jak na čtení zdrojových uploadů, tak na zápis zpracovaného výstupu, přitom to jsou dva různé buckety s dvěma různými lifecycly a nikdy neměly sdílet roli. Jakmile vzorec pojmenujete, začnete ho vidět všude, a stane se z něj jednořádkový komentář na PR místo incidentu za tři měsíce, kdy někdo zjistí, že může přepsat zdrojové soubory.
Nic z toho není exotické. Je to least privilege, ta samá disciplína, co tu byla dávno předtím, aplikovaná na proces generování kódu, který má silnou strukturální tendenci k pravému opaku. Autocomplete nikam nezmizí a upřímně ani nechceme, aby zmizel, je fakt rychlý, ale rychlý znamená, že review vrstva musí být ostřejší na tu jednu věc, ve které se model spolehlivě mýlí: rozhodnout, kolik přístupu je dost.
