なにこれ……
※事象の説明のみで結論が一部曖昧です。
!!!!!!!!!!!!!!!!!!!!!!
この記事の内容は v2.10.8 にて修正されました。
Fix Safari special character encoding issue
!!!!!!!!!!!!!!!!!!!!!!
前提
livewireのデータ改ざんの検知機能について
livewireにはリクエスト間でデータが改ざんされていないかをチェックしてくれる機能があります。
詳細は↓です。(The Checksum)
ものすごい単純に説明すると多分こんな感じ……
- データ送信時に、データとそのデータをハッシュ化した値を合わせて送信する
- 通信を受け取った側は再度データをハッシュ化する
- 1.と2.で生成されたハッシュ値を比較して同じ値なら改ざんはされていないことが保証される
ここで2つのハッシュ値に違いがあると500エラーとなります。
結合文字について
ここに「ホゲ」と「ホゲ」があります。
この二つは表示上は同じですが、「ゲ」の部分をUnicode表記にすると下記になります。
- ゲ %u30B2
- ゲ %u30B1%u3099
前者は単体でゲ(%u30B2)を表示しているのに対し、
後者はケ(%u30B1)と濁点(%u3099)の二つから「ゲ」が成されているのが分かるかと思います。
これが結合文字です。
身近(?)な例でいうと、Macで新規フォルダを作成したときの「ダ」は結合文字になっているらしいです。
(まあ、自分は普段使いがWindowsなのでMacは基本使わないんですけど)
エラーの例
今回のエラーはemitを行った際のjson内に結合文字が入っており、かつ閲覧環境がSafariの場合に発生します。
いつものようにLivewireコンポーネントを作成して、
<?php
namespace App\Http\Livewire;
class TestComponent extends \Livewire\Component
{
// ホゲ %u30DB%u30B2
// ホゲ %u30DB%u30B1%u3099 ←こっちを代入します。
public string $str = 'ホゲ';
protected $listeners = ['test'];
public function render()
{
return view('test');
}
public function test()
{
$this->str = 'テスト';
}
}
Viewではページ読み込みが完了したときにemitでtest()メソッドを呼び出して、表示を「テスト」に変更します。
(ちなみに、読み込み状態をアレコレしたいなら Loading States とかがある)
<div>
{{ $str }}
<script>
window.addEventListener("DOMContentLoaded", function () {
Livewire.emit( 'test' );
});
</script>
</div>
これで↓みたいな動作が確認できると思います。(Cromium系ブラウザ)
初期状態では$str
に「ホゲ」が入っており、ページ読み込み完了とともにemitで$str
の中身が「テスト」に変わっています。
ここまでは問題なし。
ここからが問題。
Safariで同じことを試したときにemitが実行されたタイミングで↓のようなエラーが発生してしまいます。
エラー文にはデータが壊れてるとか改竄がどうとか書いてある
Livewire encountered corrupt data when trying to hydrate the [test-component] component. Ensure that the [name, id, data] of the Livewire component wasn't tampered with between requests.
エラーの原因
livewireの処理としては
軽くlivewireの処理だけ追っていこうと思います。
前述したlivewireのハッシュ化処理は↓にて行われているようです。
https://github.com/livewire/livewire/blob/master/src/HydrationMiddleware/SecureHydrationWithChecksum.php
https://github.com/livewire/livewire/blob/master/src/ComponentChecksumManager.php
dehydrate時にchecksumとしてハッシュ値が保持され、hydrate時にはデータを再度ハッシュ化してchecksumと同じ値かどうかを確認していることが分かります。
このComponentChecksumManager::generate()
の$memo
が初回とemit時で異なる場合にエラーが発生しているようで、実際に中身を確認したところSafariで確認したときのみ「ゲ(%u30B1%u3099)」が「ゲ(%u30B2)」へと書き換わってしまっていました。
public function generate($fingerprint, $memo)
{
$hashKey = app('encrypter')->getKey();
// It's actually Ok if the "children" tracking is tampered with.
// Also, this way JavaScript can modify children as it needs to for
// dom-diffing purposes.
$memoSansChildren = array_diff_key($memo, array_flip(['children']));
$stringForHashing = ''
.json_encode($fingerprint)
.json_encode($memoSansChildren);
return hash_hmac('sha256', $stringForHashing, $hashKey);
}
Safariが悪い?
状況証拠のみで仮説しか書くことが出来ないですが、$str
に代入されている「ゲ(%u30B1%u3099)」が「ゲ(%u30B2)」へSafariによって(?)置き換えられてしまい、Livewireの改ざん検知に引っかかってエラーが出ているのではないかと考えています。
自分がJavaScriptに明るくないのもあり根底にある直接的な原因が何なのか分かっていない状態ですが、例えばMac版のChromeでは同様の事象が起きなかったので、OSの問題というわけでもない気がしています。
関連しているかは不明ですが、Safariのみで起きるUnicode関連の現象を書かれている記事があったのでとりあえず貼っておきます。
その他、何か分かる方がいればコメントで教えていただけるとありがたいです。
おまけ
「一律でこれを行えばOK」みたいな対処法は残念ながら無い気がしますが、
基本的にはデータを扱う前にUnicode正規化を挟めば良さそうかなと思いました。
Unicode正規化については分かりやすく書かれている方がいました。
感謝(*´∀人)
そもそも誰かに言われないと存在にすら気づくことも出来ないような仕様バグなので、頭の片隅に置いておくだけでもいいのかも?
それよりもこれとかこれに気を付けるべき(宣伝)