Sledování JavaScriptu:Buggy nativní JSON

Ladění je obrovskou součástí života každého softwarového inženýra:něco se pokazí a vaším úkolem je zjistit, co se stalo a jak to opravit. Čím více času strávím laděním, tím více se cítím jako detektiv, který se snaží vypátrat detaily a důkazy, aby zjistil, co se stalo. Kdykoli objevím nějakou obskurní chybu, která nás kousala, lidé se mě často ptají, jak jsem na to přišel. A tak jsem si řekl, že začnu s řadou příspěvků založených na některých nejpodivnějších chybách, se kterými jsem se setkal, v naději, že to ostatním pomůže lépe porozumět tomu, jak pracuji.

Chyba

Náš tým servisních techniků nahlásil problém na našich serverech. Přicházely požadavky, které způsobovaly chyby PHP. Kdykoli požadavky způsobují chyby na straně serveru, přirozené první místo, kde se podíváte na protokoly přístupu, abyste přesně viděli, o jaký požadavek jde. Chyba, která byla zadána, ukázala požadavek v (zhruba) následujícím formátu:

/entry?someId={}&anotherId=27&foo=true&requestId={}

Z toho bylo jasné, že požadavky jsou neplatné, protože oba someId a requestId ve skutečnosti neobsahoval identifikační údaje, pouze složené závorky. To způsobilo chybu na straně serveru, protože se PHP pokusilo použít tato neplatná ID. Ale proč se to stalo?

Vyšetřování

Normálně, když je přijat neplatný požadavek, můj první sklon je, že je to nějaký druh útoku. To se v minulosti ukázalo jako pravda, ale nezapadalo to do žádného vzoru útoku, který znám. Každý požadavek přišel se stejným formátem namísto obvyklého vzoru postupných změn, který používá většina útočníků. Takže útok byl mimo stůl. To znamenalo, že požadavek pochází z našeho kódu.

Vstupní bod použitý v požadavku je pouze pro požadavky Ajax, což znamená, že to byl kód JavaScript, který vytvořil adresu URL pro požadavek. Podle argumentů v řetězci dotazu jsem poznal, která část stránky vytvářela požadavek. Inženýr pro tuto část stránky dvakrát zkontroloval svůj kód a potvrdil, že se s nedávným vydáním nic nezměnilo. Protože všechny naše požadavky Ajaxu procházejí společnou komponentou Ajaxu, ukazuje to na změnu hlouběji v zásobníku aplikací JavaScript.

Abych se pokusil zjistit, co se stalo špatně, podíval jsem se na platný požadavek odesílaný ze stejné části stránky. Požadavek by měl být v následujícím formátu:

/entry?someId=10&anotherId=27&foo=true&requestId=5

Téměř každá hodnota argumentu řetězce dotazu je tedy číslo kromě jedné. Je zajímavé, že hodnota booleovského argumentu zůstala v pořádku, stejně jako hodnota anotherId .

Moje další zastávka byla podívat se na komponentu Ajax, abych zjistil, jestli tam došlo k nějakým změnám. Po rychlém prozkoumání registračního protokolu jsem zjistil, že se nic nezměnilo. To poukázalo na problém ještě hlubší v zásobníku aplikací JavaScript. Co se tak hluboko v zásobníku změnilo?

V tu chvíli jsem si uvědomil, že jsme právě upgradovali na nejnovější verzi YUI 3 v předchozí verzi. Mezi změnami byl i přepínač v nástroji JSON na použití nativního JSON objekt, pokud je dostupný v prohlížeči.

Teorie

Znovu jsem zkontroloval kód komponenty Ajax a zjistil jsem, že JSON.stringify() byl volán na všechny argumenty před přidáním do řetězce dotazu. To se děje proto, že hodnotami mohou být pole nebo objekty. S upgradem YUI jsem v čerstvé paměti přišel se svou první solidní teorií o problému:co když někdo používá prohlížeč, jehož nativní implementace JSON má chybu?

Poté, co jsem o tom ještě chvíli přemýšlel, zdokonalil jsem svou teorii tak, aby zahrnovala to, co jsem považoval za skutečnou chybu. Uvědomil jsem si, že ne všechna čísla byla převáděna na {}, pouze některá z nich, a rychlým pohledem do kódu jsem si uvědomil, že chybějící čísla byla s největší pravděpodobností nula. Moje teorie se pak stala, že existuje prohlížeč, který volá JSON.stringify(0) vrátí „{}“.

Důkaz

Začal jsem testovat prohlížeče, o kterých jsem věděl, že mají nativní podporu JSON, a vyšly prázdné; Chybu se mi nepodařilo reprodukovat. Cítil jsem se trochu zaražen, požádal jsem servisního technika, aby vytáhl úplné záhlaví požadavku pro daný požadavek. Když to udělal, viděl jsem něco zajímavého v řetězci user-agent:

Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.1b1) Gecko/20081007 Firefox/3.1b1

Fascinující. Zdá se, že osoba, u které se tato chyba vyskytuje, ve skutečnosti používá Firefox 3.1 Beta 1. Pro ty, kteří to nevědí, se Firefox 3.1 po třetí beta verzi stal Firefoxem 3.5 (tj. neexistovala žádná GA Firefoxu 3.1). To znamená, že někdo z neznámého důvodu používá Firefox 3.1 Beta 1. Ale je to problémový prohlížeč?

Zeptal jsem se našeho servisního technika, jak často se tato chyba vyskytuje. Odpověděl, že je to poměrně často. Nedokázal jsem si představit, že by Firefox 3.1 Beta 1 používal tolik lidí, takže jsem si nebyl jistý, jestli to byl zdroj problému nebo ne. Požádal jsem ho, aby vytáhl několik dalších problémových požadavků, doplněných hlavičkami požadavků, abych je mohl prohlédnout. To potvrdilo, že každý uživatel, který se setkal s tímto problémem, ve skutečnosti používal Firefox 3.1 Beta 1.

Tím ale dobrý detektiv nekončí. Jediné, co jsem dokázal, bylo, že všichni uživatelé používali stejný prohlížeč. Zdroj problému jsem neuvedl. Po dlouhém hledání se mi podařilo najít instalační program Firefox 3.1 Beta 1 na FileHippo. Nainstaloval jsem prohlížeč a přidal Firebug. Otevřel jsem konzoli Firebug a napsal JSON.stringify(0) . Výstup byl {}. Záhada vyřešena.

Následky

Nahlásil jsem problém YUI jako něco, co by se mělo řešit. Krátkodobě jsem opravil naši vlastní verzi nástroje JSON tak, aby nikdy nepoužívala nativní JSON.stringify() metoda. Dopad tohoto rozhodnutí na výkon mě neznepokojoval, protože většina prohlížečů našich uživatelů nativně nepodporuje JSON a serializujeme pouze velmi malé množství dat. Konzistence a odstranění chyby je mnohem důležitější než několik milisekund, které ušetříme použitím nativního JSON.stringify() metoda.