Ons het ons eie auth-bediener voor bekendstelling geoudit. Die foute het 'n vorm gehad.
Voordat ons iemand gevra het om ons met hulle aanmeldings te vertrou, het ons ons eie auth-bediener deurgegaan soos 'n aanvaller sou: nie deur dit te laat loop nie, maar deur dit te lees. Wat uitgekom het, was 'n geskrewe hersiening van die hele oppervlak: sestien genommerde afdelings, 261 reëls, van SAML deur OIDC-federasie, SCIM, die admin-API, MFA en die rugsteungereedskap, met elke bevinding wat 'n erns en 'n bronligging dra. Agt is gemerk om eerste reggemaak te word. Ons het daardie agt in een aand reggemaak en die res van die lys 'n halfuur later. Twee en 'n half weke daarna het ons dieselfde oefening oor die multihuurder-beheervlak gedoen en nog vyf gekry, gesluit die aand voordat ons produksie toe is.
Hier is die getal wat bygebly het. Die toetsstel het 367 uit 367 geslaag gerapporteer voor daardie aand, en 367 uit 367 daarna. Niks het rooi geword nie, want niks in die stel was op enigiets daarvan gerig nie. Toetse bewys wat jy gedink het om te toets, en niemand het daaraan gedink om te toets of die verwydering van 'n XML-attribuut wat die handtekening nie dek nie, 'n herspeelde bewering langs 'n vriendeliker tak sou stuur nie.
Gelees as agt onverwante foute, is 'n lys soos hierdie vir enigiemand anders nutteloos. Gelees as vorms, is dit drie gewoontes, herhaal oor substelsels heen, in kode geskryf deur mense wat die spesifikasies gelees het.
Die client_id, die aangestuurde kopstuk, die InResponseTo en die e-poseis
Die eerste gewoonte is om 'n waarde wat die oproeper skryf as 'n feit oor die oproeper te behandel. Die twee wat die hersiening as Krities gemerk het, was elk 'n enkele invoer: 'n navraagparameter, en 'n e-poseis.
MFA-beleid is afgelei van die client_id wat uit die returnUrl op die aanmeldvorm ontleed is, en /connect/authorize het nooit weer nagegaan nie. Rig die aanmelding op 'n kliënt met MFA afgeskakel en die sessie het sonder 'n tweede faktor uitgekom, en is toe volkome goed bestee by die kliënt wat een vereis het. Daardie een het sy eie skrywe.
Per-IP-limiete het op die afgeleë adres gesleutel, en aangestuurde kopstukke is vanaf 0.0.0.0/0 vertrou. Een X-Forwarded-For-waarde per versoek het elke per-IP-beheermaatreël bloot adviserend gemaak, en die IP-kolom in die ouditlog as bonus herskryf. Die besonderhede is hier.
Die SAML-beweringsverbruiker het tussen die SP-geïnisieerde pad (verbruik die versoek-id, eenmalige gebruik) en die IdP-geïnisieerde pad (gaan 'n bewerings-id-kas na) gekies bloot op grond van of die respons 'n InResponseTo-attribuut gedra het. Daardie attribuut sit op die <Response>-element, wat nie gedek word wanneer net die bewering onderteken is nie, wat die algemene geval is. Verwyder dit en 'n vasgevangde respons het die tak geneem waarvan die oorspronklike aanmelding die herspeelkas nooit gevul het nie, met elke handtekening wat steeds verifieer. Waarom die handtekening nooit die probleem was nie.
En terugkerende gefedereerde gebruikers is opgelos deur die e-pos in die bewering, so enige verbinding kon iemand anders se adres beweer en op hulle rekening beland. Daardie een is ook opgeskryf.
Die admin-API was 'n fabriek vir sy eie geloofsbrief
Admin-magtiging het op presies een ding gesleutel: 'n token waarvan die scope-eis die admin-bestek gedra het. Twee admin-eindpunte sou daardie bestek in 'n vars token plaas.
POST /api/v1/token, die nabootsingseindpunt wat ondersteuningsgereedskap gebruik, het sy scopes-navraagparameter opgedeel en die resultaat woordeliks in 'n toegangstoken en 'n verfrissingstoken gekopieer, sonder enige toets teen die kliënt se geregistreerde AllowedScopes. Een admin-geloofsbrief, hoe kortstondig ook al, het dus 'n langlewende admin-verfrissingstoken vir enige gebruiker en enige kliënt geslaan: bevoorregting wat langer leef as die rotasie van die geloofsbrief waaroor jy bekommerd was. Kliëntskepping het intussen 'n rou kliëntobjek direk van die versoekliggaam af gebind, ClientSecretHashes ingesluit, so 'n admin kon 'n client_credentials-kliënt registreer wat die admin-bestek hou en 'n geheime hash wat hulle reeds geken het. Lys en kry het elke gestoorde hash teruggekaats.
Die nabootsingseindpunt weier nou die admin-bestek en beperk die res tot die kliënt se eie AllowedScopes, skep en opdateer bind 'n DTO sonder 'n geheime-hash-veld, en geen respons dra 'n hash nie. 'n Toestemming wat homself kan uitreik, is nie 'n toestemming nie.
'n Koppelteken, 'n groep sonder eienaar, en 'n e-pos wat 'n identiteit se werk doen
Die tweede gewoonte is om iets met 'n naam daarin te laat instaan vir 'n identiteit.
E-pos was die aansluitsleutel vir gefedereerde aanmelding: globaal, teenwoordig by baie verskaffers, en presies wat die verbinding wat dit beweer sê dit is. Die oplossing was om op die paar van verskaffer en subjek op te los, wat geen ander verbinding kan slaan nie.
In die beheervlak was die huurder-slug die naam. Die roeteerder wat 'n slug in 'n stoortabelvoorvoegsel omskakel, het koppeltekens gestroop, so acme-corp en acmecorp het dieselfde voorvoegsel opgelewer, wat dieselfde tabelstel en dieselfde Vault-ondertekeningsleutel beteken het. Twee afsonderlik registreerbare huurders wat een tabelstel en een ondertekeningsleutel deel. Registreer die tweede en jy kyk na die eerste een se gebruikers. Die oplossing was nie 'n slimmer voorvoegselfunksie nie. Dit was om koppeltekens in slugs te verbied sodat die afbeelding injektief is, en dit is die langer weergawe werd.
SCIM was die een wat gebrand het, want die helfte daarvan was reg. Elke gebruikerslees en -skryf het weer nagegaan dat die hulpbron deur die oproepende kliënt voorsien is. Groepe, in die lêer langsaan, het glad geen eienaar gehad nie: skep het nooit een gestempel nie, lys het null as die eienaarsfilter deurgegee en elke groep in die omgewing teruggegee, en kry, vervang, laslap en skrap het enige id aanvaar. Om 'n groep-id te ken was die hele magtiging, en waar verskeie voorsieningskliënte 'n omgewing deel, is dit kruis-kliënt-lees en -skryf van groeplidmaatskap, wat is hoe rolle toegeken word. Groepe dra nou die skeppende kliënt, en elke lees en skryf word daartoe beperk.
Die koekie is onderteken voordat die webhook geantwoord het
Die derde gewoonte is om die onomkeerbare ding te doen voordat die toets uitgevoer word.
Die duidelikste geval is 'n kenmerk wat klante koop: 'n huurder registreer 'n webhook wat by verifikasietyd gevra word "laat hierdie aanmelding toe?", en kan dit stel om die antwoord af te dwing. Die haak het op elke pad wat 'n sessie vestig geloop, korrek. Dit het net ná SignInAsync geloop. So 'n weiering het gegooi, die versoek het met 'n 500 teruggekom, en die blaaier het reeds 'n ondertekende sessiekoekie gehou. Enige kliënt wat die fout geïgnoreer het, was aangemeld, geblokkeer of nie. Die aanmeldblokkerende webhook het nie geblokkeer nie.
Die oplossing is een reël wat geskuif is, ses keer: loop die haak voor SignInAsync op wagwoord, OIDC, SAML, MFA-verifieer en MFA-opstelling, en voordat tokens by die tokeneindpunt geslaan word. Die biblioteekverandering en die beheervlakverandering het twee minute uitmekaar geland. 'n Weiering is nou 'n skoon 403 sonder Set-Cookie, en 'n end-tot-end-toets bevestig dat 'n geweierde aanmelding geen .AspNetCore.Cookies uitreik nie.
Dieselfde volgorde het op drie stiller plekke opgeduik. Magtigingskodes is gelees en dan geskrap as twee bewerkings, so twee gelyktydige inlossings van een kode kon albei die lees slaag en albei tokens kry; die toekenningswinkel stel nou TryConsumeAsync beskikbaar, 'n voorwaardelike skrapping wat net waar teruggee aan die oproeper wat die ry werklik verwyder het. Herstel het entiteite opgevoeg voordat enigiets geverifieer is: die FileHashes-veld is gedokumenteer as geverifieer tydens herstel terwyl dit nooit gevul of nagegaan is nie, wat 'n gepeuterde rugsteun 'n skryfprimitief teen produksie maak. En die Stripe-webhook het op gebeurtenisse opgetree voordat dit aangeteken het dat dit hulle gesien het, so heraflewering wat ten minste een keer gebeur kon 'n huurder se plan en status laat flikker of aanmanings weer stuur. Al drie gaan nou eers na.
Om die URL te valideer wanneer dit gestoor word, bewys niks wanneer dit geskakel word nie
Klante gee vir ons URL's: auth-webhooks, rugsteunteikens, retensie-terugroepe. Om een te weier wat na 'n private adres oplos op die oomblik wat dit gestoor word, is die ooglopende wag, en op sy eie is dit byna waardeloos, want DNS is nie 'n belofte nie. 'n Naam wat publiek oplos wanneer jy dit valideer, kan na 169.254.169.254 oplos wanneer jy koppel.
So die draende toets woon nie in die instellingshanteerder nie. Dit is 'n koppel-terugroep op die HTTP-hanteerder self. Elke uitgaande oproep na 'n URL wat 'n klant verskaf het, los die gasheer op, weier loopback, RFC1918, unique-local en die hele 169.254.0.0/16 skakel-plaaslike reeks waarin die wolk-metadatadiens woon, en open dan die sok na presies daardie gevalideerde adres in plaas van na die gasheernaam, sodat daar geen tweede oplossing oor is om te herbind nie. Outomatiese herleidings is af, want 'n 302 is nog een kans om 'n interne teiken te noem. Die stoortyd-toets bly as 'n foutboodskap vir die klant, wat al is waarvoor dit ooit goed was.
Twee eindpunte en 'n rugsteunlêer wat net die netwerk beskerm het
/_internal/cluster/gossip is anoniem afgebeeld, en sy gedeelde-geheim-toets het as 'n kommentaar bestaan eerder as kode. /_internal/backchannel-logout is anoniem afgebeeld met antivervalsing afgeskakel op die openbare luisteraar, het 'n liggaam van {"SubjectId": "..."} geneem, en elke toekenning wat daardie subjek gehou het, herroep. Nie een het enige in-proses-wag gehad nie. Die enigste ding voor hulle was ingangskonfigurasie, en gegewe die aangestuurde-kopstuk-bevinding hierbo, was enige IP-gebaseerde verdediging in elk geval vervalsbaar. Albei vereis nou 'n gedeelde geheim wat in konstante tyd vergelyk word, of 'n interne bronadres wanneer geen geheim gekonfigureer is nie.
Rugsteune het dieselfde karakter gehad. Op gasheerrekenaars wat die plaaslike sleutelbron gebruik, hou die ondertekeningsleuteltabel die EC-private skalaar, en rugsteun het elke kolom woordeliks na gewone teks in gzip-JSONL geserialiseer. Enigiemand wat 'n rugsteunlêer kon lees, kon tokens vervals vir die uitreiker waarvandaan dit gekom het. Daardie tabel word nou by verstek uitgesluit met 'n uitdruklike inskrywing vir geënkripteerde teikens, en elke datalêer word met SHA-256 in die manifes gehash en geverifieer voordat herstel 'n enkele ry skryf.
Waar 'n toets loop, is die meeste van wat die toets werd is
Nie een hiervan was 'n ontbrekende toets in die sin dat niemand daaraan gedink het nie. MFA is geïmplementeer en getoets. Die webhook-haak het op elke pad geloop. Die SSRF-validering het bestaan. SCIM-gebruikers is op elke werkwoord korrek beperk, in die lêer langs die een waar groepe dit nie was nie. In byna elke geval was die logika geskryf, korrek, en geïnstalleer op 'n plek waar dit nie sy werk kon doen nie: op 'n waarde wat die oproeper verskaf het, by 'n stap wat die oproeper kon oorslaan, ná die toestand wat dit veronderstel was om te voorkom, of op die hulpbron langsaan.
So die nuttige vraag by elke eindpunt is nie "valideer ons dit" nie. Dit is drie ander. Wat bewys hierdie versoek werklik, teenoor wat dit beweer? Is hierdie identifiseerder iets wat 'n oproeper kan noem, of iets wat net die regte oproeper kan hou? En gebeur enigiets onomkeerbaars voordat die antwoord aankom? 'n Geskrewe hersiening, afdeling vir afdeling deurgewerk met 'n erns op elke bevinding, is meestal 'n toestel om daardie drie verby die kode te dwing waaroor jy seker is. Ons s'n was seker. Dit was sestien afdelings vol sekerheid, plus agt dinge wat iemand se voorval sou geword het.
Niks daarvan was vir 'n klant bereikbaar nie, want daar was nog geen klante nie. Dit is die argument om dit voor bekendstelling te doen eerder as ná die eerste verslag: dieselfde lys, ses maande later geskryf, is 'n openbaarmakingstydlyn in plaas van een aand se werk. As jy die huidige weergawe van die antwoorde wil hê, sit ons sekuriteitsbladsy uiteen hoe huurders geïsoleer word, wat in rus geënkripteer is, en wat 'n rugsteun van jou data werklik bevat.