Skryté zprávy v názvech vlastností JavaScriptu

Nedávno jsem narazil na tento tweet od @FakeUnicode. Zahrnoval úryvek JavaScriptu, který vypadal docela neškodně, ale vedl k upozornění na skrytou zprávu. Chvíli mi trvalo, než jsem pochopil, co se děje, a tak jsem si řekl, že zdokumentování kroků, které jsem podnikl, by mohlo být pro někoho zajímavé.

Fragment byl následující:

for(A in {A󠅬󠅷󠅡󠅹󠅳󠄠󠅢󠅥󠄠󠅷󠅡󠅲󠅹󠄠󠅯󠅦󠄠󠅊󠅡󠅶󠅡󠅳󠅣󠅲󠅩󠅰󠅴󠄠󠅣󠅯󠅮󠅴󠅡󠅩󠅮󠅩󠅮󠅧󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄮󠄠󠅎󠅯󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄠󠄽󠄠󠅳󠅡󠅦󠅥󠄡:0}){
  alert(unescape(escape(A).replace(/u.{8}/g,[])))
};

Takže, co očekáváte, že se zde stane?

Používá for in smyčka, která iteruje přes vyčíslitelné vlastnosti objektu. Existuje pouze vlastnost A v tom jsem si myslel, že je to upozornění zobrazující se s písmenem A . No... mýlil jsem se. :D

To mě překvapilo a začal jsem ladit pomocí konzole Chrome.

Objevování skrytých bodů kódu

První věc, kterou jsem udělal, bylo zjednodušení úryvku, abych viděl, co se děje.

for(A in {A:0}){console.log(A)};
// A

Hmm... dobře, tady se nic neděje. Tak jsem pokračoval.

for(A in {A:0}){console.log(escape(A))};
// A%uDB40%uDD6C%uDB40%uDD77%uDB40%uDD61%uDB40%uDD79%uDB40%uDD73%uDB40%uDD20%uDB40%uDD62%uDB40%uDD65%uDB40%uDD20%uDB40%uDD77%uDB40%uDD61%uDB40%uDD72%uDB40%uDD79%uDB40%uDD20%uDB40%uDD6F%uDB40%uDD66%uDB40%uDD20%uDB40%uDD4A%uDB40%uDD61%uDB40%uDD76%uDB40%uDD61%uDB40%uDD73%uDB40%uDD63%uDB40%uDD72%uDB40%uDD69%uDB40%uDD70%uDB40%uDD74%uDB40%uDD20%uDB40%uDD63%uDB40%uDD6F%uDB40%uDD6E%uDB40%uDD74%uDB40%uDD61%uDB40%uDD69%uDB40%uDD6E%uDB40%uDD69%uDB40%uDD6E%uDB40%uDD67%uDB40%uDD20%uDB40%uDD71%uDB40%uDD75%uDB40%uDD6F%uDB40%uDD74%uDB40%uDD65%uDB40%uDD73%uDB40%uDD2E%uDB40%uDD20%uDB40%uDD4E%uDB40%uDD6F%uDB40%uDD20%uDB40%uDD71%uDB40%uDD75%uDB40%uDD6F%uDB40%uDD74%uDB40%uDD65%uDB40%uDD73%uDB40%uDD20%uDB40%uDD3D%uDB40%uDD20%uDB40%uDD73%uDB40%uDD61%uDB40%uDD66%uDB40%uDD65%uDB40%uDD21

Svatý! Odkud to všechno pochází?

Tak jsem udělal krok zpět a podíval se na délku provázku.

for(A in {A󠅬󠅷󠅡󠅹󠅳󠄠󠅢󠅥󠄠󠅷󠅡󠅲󠅹󠄠󠅯󠅦󠄠󠅊󠅡󠅶󠅡󠅳󠅣󠅲󠅩󠅰󠅴󠄠󠅣󠅯󠅮󠅴󠅡󠅩󠅮󠅩󠅮󠅧󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄮󠄠󠅎󠅯󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄠󠄽󠄠󠅳󠅡󠅦󠅥󠄡:0}){console.log(A.length)};
// 129

Zajímavý. Dále jsem zkopíroval A z objektu a již zjistili, že konzole Chrome se zabývá něčím, co je zde skryté, protože kurzor se „zasekl“ na několik stisků levé/pravé klávesy.

Ale pojďme se podívat na to, co tam je, a získat hodnoty všech 129 kódových jednotek:

const propertyName = 'A󠅬󠅷󠅡󠅹󠅳󠄠󠅢󠅥󠄠󠅷󠅡󠅲󠅹󠄠󠅯󠅦󠄠󠅊󠅡󠅶󠅡󠅳󠅣󠅲󠅩󠅰󠅴󠄠󠅣󠅯󠅮󠅴󠅡󠅩󠅮󠅩󠅮󠅧󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄮󠄠󠅎󠅯󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄠󠄽󠄠󠅳󠅡󠅦󠅥󠄡';
for(let i = 0; i < propertyName.length; i++) {
  console.log(propertyName[i]);
  // to get code unit values use charCodeAt
  console.log(propertyName.charCodeAt(i));
}
// A
// 65
// �
// 56128
// �
// 56684
// ...

To, co tam vidíte, je písmeno A který má hodnotu jednotky kódu 65 následuje několik jednotek kódu někde kolem 55 a 56 tisíc, což je console.log jsou zobrazeny se známým otazníkem, což znamená, že systém neví, jak s touto kódovou jednotkou zacházet.

Náhradní páry v JavaScriptu

Tyto hodnoty jsou části tzv. náhradních párů, které se používají k reprezentaci kódových bodů, které mají hodnotu větší než 16 bitů (nebo jinými slovy mají hodnotu kódového bodu větší než 65536 ). To je potřeba, protože samotný Unicode definuje 1 114 112 různých kódových bodů a formát řetězce používaný JavaScriptem je UTF-16. To znamená, že pouze prvních 65536 kódových bodů definovaných v Unicode může být reprezentováno v jediné kódové jednotce v JavaScriptu.

Větší hodnotu pak lze vyhodnotit použitím bláznivého vzorce na pár, což má za následek, že hodnota je větší než 65536 .

Shameless plug:Přednáším přesně na toto téma, které vám může pomoci pochopit koncepty kódových bodů, emotikonů a náhradních párů.

Takže to, co jsme objevili, bylo 129 kódových jednotek, z nichž 128 jsou náhradní páry představující 64 kódových bodů. Co jsou tedy tyto body kódu?

Pro načtení hodnot kódu z řetězce je opravdu šikovný for of smyčka, která iteruje přes body kódu řetězce (a ne přes jednotky kódu jako první for smyčka) a také ... operátor, který používá for of pod kapotou.

console.log([...'A󠅬󠅷󠅡󠅹󠅳󠄠󠅢󠅥󠄠󠅷󠅡󠅲󠅹󠄠󠅯󠅦󠄠󠅊󠅡󠅶󠅡󠅳󠅣󠅲󠅩󠅰󠅴󠄠󠅣󠅯󠅮󠅴󠅡󠅩󠅮󠅩󠅮󠅧󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄮󠄠󠅎󠅯󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄠󠄽󠄠󠅳󠅡󠅦󠅥󠄡']);
// (65) ["A", "󠅬", "󠅷", "󠅡", "󠅹", "󠅳", "󠄠", "󠅢", "󠅥", "󠄠", "󠅷", "󠅡", "󠅲", "󠅹", "󠄠", "󠅯", "󠅦", "󠄠", "󠅊", "󠅡", "󠅶", "󠅡", "󠅳", "󠅣", "󠅲", "󠅩", "󠅰", "󠅴", "󠄠", "󠅣", "󠅯", "󠅮", "󠅴", "󠅡", "󠅩", "󠅮", "󠅩", "󠅮", "󠅧", "󠄠", "󠅱", "󠅵", "󠅯", "󠅴", "󠅥", "󠅳", "󠄮", "󠄠", "󠅎", "󠅯", "󠄠", "󠅱", "󠅵", "󠅯", "󠅴", "󠅥", "󠅳", "󠄠", "󠄽", "󠄠", "󠅳", "󠅡", "󠅦", "󠅥", "󠄡"]

Takže console.log ani neví, jak zobrazit tyto výsledné body kódu, takže se podívejme, s čím se podrobně zabýváme.

// to get code point values use codePointAt
console.log([...'A󠅬󠅷󠅡󠅹󠅳󠄠󠅢󠅥󠄠󠅷󠅡󠅲󠅹󠄠󠅯󠅦󠄠󠅊󠅡󠅶󠅡󠅳󠅣󠅲󠅩󠅰󠅴󠄠󠅣󠅯󠅮󠅴󠅡󠅩󠅮󠅩󠅮󠅧󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄮󠄠󠅎󠅯󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄠󠄽󠄠󠅳󠅡󠅦󠅥󠄡'].map(c => c.codePointAt(0)));
// [65, 917868, 917879, ...]

Poznámka:uvědomte si, že při práci s jednotkami kódu a body kódu v JavaScriptu existují dvě různé funkce 👉🏻 charCodeAt a codePointAt. Chovají se trochu jinak, takže se možná budete chtít podívat.

Názvy identifikátorů v objektech JavaScript

Kód ukazuje 917868 , 917879 a následující jsou součástí doplňku Variation Selectors Supplement v Unicode. Selektory variací v Unicode se používají ke specifikaci standardizovaných sekvencí variací pro matematické symboly, symboly emoji, písmena 'Phags-pa a unifikované ideografy CJK odpovídající ideografům kompatibility CJK. Obvykle nejsou určeny k použití samostatně.

Dobře, ale proč na tom záleží?

Když přejdete ke specifikaci ECMAScript, zjistíte, že názvy identifikátorů vlastností mohou obsahovat více než jen „normální znaky“.

Identifier ::
  IdentifierName but not ReservedWord
IdentifierName ::
  IdentifierStart
  IdentifierName IdentifierPart
IdentifierStart ::
  UnicodeLetter
  $
  _
  \ UnicodeEscapeSequence
IdentifierPart ::
  IdentifierStart
  UnicodeCombiningMark
  UnicodeDigit
  UnicodeConnectorPunctuation
  <ZWNJ>
  <ZWJ>

Výše vidíte tedy, že identifikátor se může skládat z IdentifierName a IdentifierPart . Důležitou částí je definice pro IdentifierPart . Pokud to není první znak identifikátoru, jsou následující názvy identifikátorů zcela platné:

const examples = {
  // UnicodeCombiningMark example
  somethingî: 'LATIN SMALL LETTER I WITH CIRCUMFLEX',
  somethingi\u0302: 'I + COMBINING CIRCUMFLEX ACCENT',
  
  // UnicodeDigit example
  something١: 'ARABIC-INDIC DIGIT ONE',
  something\u0661: 'ARABIC-INDIC DIGIT ONE',
  
  // UnicodeConnectorPunctuation example
  something﹍: 'DASHED LOW LINE',
  something\ufe4d: 'DASHED LOW LINE',
  
  // ZWJ and ZWNJ example
  something\u200c: 'ZERO WIDTH NON JOINER',
  something\u200d: 'ZERO WIDTH JOINER'
}

Takže když vyhodnotíte tento výraz, dostanete následující výsledek

{
  somethingî: "ARABIC-INDIC DIGIT ONE",
  somethingî: "I + COMBINING CIRCUMFLEX ACCENT",
  something١: "ARABIC-INDIC DIGIT ONE"
  something﹍: "DASHED LOW LINE",
  something: "ZERO-WIDTH NON-JOINER",
  something: "ZERO-WIDTH JOINER"
}

To mě přivádí k dnešnímu učení. 🎉

Podle specifikace ECMAScript:

To znamená, že dva klíče identifikátoru objektu mohou vypadat úplně stejně, ale mohou se skládat z různých kódových jednotek, což znamená, že oba budou součástí objektu. Jako v tomto případě který má hodnotu jednotky kódu 00ee a znak i s koncovým COMBINING CIRCUMFLEX ACCENT . Nejsou tedy stejné a vypadá to, že máte ve svém objektu zahrnuty zdvojené vlastnosti. Totéž platí pro klávesy s koncovou spojkou Zero-Width nebo bez spojky s nulovou šířkou. Vypadají stejně, ale nejsou!

Ale zpět k tématu:hodnoty doplňku Variation Selectors Supplement, které jsme našli, patří do UnicodeCombiningMark kategorie, která z nich činí platný název identifikátoru (i když nejsou viditelné). Jsou neviditelné, protože systém s největší pravděpodobností zobrazí jejich výsledek pouze při použití v platné kombinaci.

Funkce escape a náhrada některých řetězců

Co tedy escape funkce spočívá v tom, že přejde přes všechny jednotky kódu a unikne z každé jednotky. To znamená, že vezme počáteční písmeno A a všechny části náhradních párů a jednoduše je znovu přemění na struny. Hodnoty, které nebyly viditelné, budou "stringifikované". Toto je dlouhá sekvence, kterou jste viděli na začátku článku.

A%uDB40%uDD6C%uDB40%uDD77%uDB40%uDD61%uDB40%uDD79%uDB40%uDD73%uDB40%uDD20%uDB40%uDD62%uDB40%uDD65%uDB40%uDD20%uDB40%uDD77%uDB40%uDD61%uDB40%uDD72%uDB40%uDD79%uDB40%uDD20%uDB40%uDD6F%uDB40%uDD66%uDB40%uDD20%uDB40%uDD4A%uDB40%uDD61%uDB40%uDD76%uDB40%uDD61%uDB40%uDD73%uDB40%uDD63%uDB40%uDD72%uDB40%uDD69%uDB40%uDD70%uDB40%uDD74%uDB40%uDD20%uDB40%uDD63%uDB40%uDD6F%uDB40%uDD6E%uDB40%uDD74%uDB40%uDD61%uDB40%uDD69%uDB40%uDD6E%uDB40%uDD69%uDB40%uDD6E%uDB40%uDD67%uDB40%uDD20%uDB40%uDD71%uDB40%uDD75%uDB40%uDD6F%uDB40%uDD74%uDB40%uDD65%uDB40%uDD73%uDB40%uDD2E%uDB40%uDD20%uDB40%uDD4E%uDB40%uDD6F%uDB40%uDD20%uDB40%uDD71%uDB40%uDD75%uDB40%uDD6F%uDB40%uDD74%uDB40%uDD65%uDB40%uDD73%uDB40%uDD20%uDB40%uDD3D%uDB40%uDD20%uDB40%uDD73%uDB40%uDD61%uDB40%uDD66%uDB40%uDD65%uDB40%uDD21

Trik je nyní v tom, že @FakeUnicode vybral specifické selektory variací, jmenovitě ty, které končí číslem, které mapuje zpět na skutečný znak. Podívejme se na příklad.

// a valid surrogate pair sequence
'%uDB40%uDD6C'.replace(/u.{8}/g,[]);
// %6C 👉🏻 6C (hex) === 108 (dec) 👉🏻 LATIN SMALL LETTER L
unescape('%6C')
// 'l'

Jedna věc, která vypadá trochu záhadně, je, že příklad používá prázdné pole [] jako hodnotu pro nahrazení řetězce, která bude vyhodnocena pomocí toString() což znamená, že se vyhodnotí jako '' .

Práci udělá i prázdný řetězec. Důvod, proč jít s [] je, že tímto způsobem můžete obejít filtr uvozovek nebo něco podobného.

Tímto způsobem je možné zakódovat celou zprávu neviditelnými znaky.

Celková funkčnost

Takže když se znovu podíváme na tento příklad:

for(A in {A󠅬󠅷󠅡󠅹󠅳󠄠󠅢󠅥󠄠󠅷󠅡󠅲󠅹󠄠󠅯󠅦󠄠󠅊󠅡󠅶󠅡󠅳󠅣󠅲󠅩󠅰󠅴󠄠󠅣󠅯󠅮󠅴󠅡󠅩󠅮󠅩󠅮󠅧󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄮󠄠󠅎󠅯󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄠󠄽󠄠󠅳󠅡󠅦󠅥󠄡:0}){
  alert(unescape(escape(A).replace(/u.{8}/g,[])))
};

Co se stane:

  • A󠅬󠅷󠅡󠅹󠅳󠄠󠅢󠅥󠄠󠅷󠅡󠅲󠅹󠄠󠅯󠅦󠄠󠅊󠅡󠅶󠅡󠅳󠅣󠅲󠅩󠅰󠅴󠄠󠅣󠅯󠅮󠅴󠅡󠅩󠅮󠅩󠅮󠅧󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄮󠄠󠅎󠅯󠄠󠅱󠅵󠅯󠅴󠅥󠅳󠄠󠄽󠄠󠅳󠅡󠅦󠅥󠄡:0 - A obsahuje spoustu „skrytých kódových jednotek“
  • tyto znaky se zviditelní pomocí escape
  • mapování se provádí pomocí replace
  • výsledek mapování bude znovu bez escapování a zobrazí se v okně upozornění

Myslím, že je to docela skvělá věc!

Další zdroje

Tento malý příklad pokrývá mnoho témat Unicode. Takže pokud si chcete přečíst více, vřele vám doporučuji přečíst si články Mathiase Bynense o Unicode a JavaScriptu:

  • JavaScript má problém s kódováním Unicode
  • JavaScriptové sekvence escape znaků