[{"data":1,"prerenderedAt":760},["ShallowReactive",2],{"content-/chrome-extension-storage-security":3,"all-pages-for-dir":758,"og-image-/chrome-extension-storage-security":759},{"id":4,"title":5,"body":6,"category":740,"description":741,"extension":742,"meta":743,"navigation":256,"path":744,"project_name":745,"published":746,"publishedAt":747,"seo":748,"stem":749,"tags":750,"todo":755,"updatedAt":756,"__hash__":757},"pages/2026-04/2026-04-07/chrome-extension-storage-security.md","Chrome拡張のsessionStorage脱却とTwitterアーカイブ11,669件のSQLite化",{"type":7,"value":8,"toc":722},"minimark",[9,14,18,21,26,31,39,42,46,53,63,66,68,72,75,82,142,146,164,349,356,359,372,541,544,546,550,557,560,563,565,569,573,580,584,587,672,675,678,681,684,686,689,718],[10,11,13],"h1",{"id":12},"chrome拡張のsessionstorage脱却とtwitterアーカイブのsqlite化","Chrome拡張のsessionStorage脱却とTwitterアーカイブのSQLite化",[15,16,17],"p",{},"ストレージのセキュリティ調査中に、sessionStorageの中身が他の拡張のcontent scriptから丸見えになっていることに気づいた。ブラウザのDevToolsでApplicationタブを開き、sessionStorageの項目を眺めていたら、この拡張が書き込んだ値に別の拡張からもアクセスできる構造が目に入った。ここから、残っていたsessionStorage参照をすべてchrome.storage.localに書き換える作業が始まった。",[19,20],"hr",{},[22,23,25],"h2",{"id":24},"sessionstorageの何が問題だったか","sessionStorageの何が問題だったか",[27,28,30],"h3",{"id":29},"同一ページ上のcontent-scriptは境界がない","同一ページ上のcontent scriptは境界がない",[15,32,33,34,38],{},"Chrome拡張のcontent scriptは、注入先のWebページのDOM・localStorage・sessionStorageを共有する。つまり、自分の拡張がsessionStorageに書き込んだ値を、同じページに注入された別の拡張のcontent scriptがそのまま",[35,36,37],"code",{},"window.sessionStorage.getItem()","で読み取れる。",[15,40,41],{},"APIのエンドポイントやサービス名など、外部に見せたくない情報がsessionStorageに入っていれば、悪意ある拡張がそれを吸い出すシナリオが成り立つ。",[27,43,45],{"id":44},"chromestoragelocalは拡張ごとに隔離される","chrome.storage.localは拡張ごとに隔離される",[15,47,48,49,52],{},"一方、",[35,50,51],{},"chrome.storage.local","はChrome拡張のAPI層で管理されており、拡張ごとに完全に分離されたストレージ空間を持つ。他の拡張からは物理的にアクセスできない。",[54,55,60],"pre",{"className":56,"code":58,"language":59},[57],"language-text","sessionStorage     → ページ単位で共有、content script間で丸見え\nchrome.storage.local → 拡張単位で隔離、他の拡張からアクセス不可\n","text",[35,61,58],{"__ignoreMap":62},"",[15,64,65],{},"この拡張は実はchrome.storage.localを主に使っていた。だが、sessionStorageへの参照が数箇所に残っていた。そこを潰す作業に入った。",[19,67],{},[22,69,71],{"id":70},"移行作業-4ファイルのsessionstorage参照を書き換える","移行作業: 4ファイルのsessionStorage参照を書き換える",[27,73,74],{"id":74},"残存箇所の洗い出し",[15,76,77,78,81],{},"プロジェクト全体を",[35,79,80],{},"sessionStorage","でgrepしたら、4ファイルがヒットした。",[83,84,85,98],"table",{},[86,87,88],"thead",{},[89,90,91,95],"tr",{},[92,93,94],"th",{},"ファイル",[92,96,97],{},"用途",[99,100,101,112,122,132],"tbody",{},[89,102,103,109],{},[104,105,106],"td",{},[35,107,108],{},"storage.js",[104,110,111],{},"ストレージアクセスの抽象化レイヤー",[89,113,114,119],{},[104,115,116],{},[35,117,118],{},"export.js",[104,120,121],{},"ブックマークのCSV/Sheets出力",[89,123,124,129],{},[104,125,126],{},[35,127,128],{},"bridge.js",[104,130,131],{},"content scriptとbackground間のメッセージブリッジ",[89,133,134,139],{},[104,135,136],{},[35,137,138],{},"import.js",[104,140,141],{},"外部データの取り込み処理",[27,143,145],{"id":144},"getservicenameの同期非同期化","getServiceNameの同期→非同期化",[15,147,148,149,152,153,156,157,160,161,163],{},"移行で一つ引っかかったのが",[35,150,151],{},"getServiceName","関数だった。sessionStorageの",[35,154,155],{},"getItem()","は同期関数だが、",[35,158,159],{},"chrome.storage.local.get()","はPromiseを返す。つまり",[35,162,151],{},"をasync化する必要がある。",[54,165,169],{"className":166,"code":167,"language":168,"meta":62,"style":62},"language-javascript shiki shiki-themes vitesse-light vitesse-light","// Before: 同期で値を取得\nfunction getServiceName() {\n  return sessionStorage.getItem('serviceName') || 'default';\n}\n\n// After: chrome.storage.local は非同期\nasync function getServiceName() {\n  const result = await chrome.storage.local.get('serviceName');\n  return result.serviceName || 'default';\n}\n","javascript",[35,170,171,180,198,245,251,258,264,279,323,344],{"__ignoreMap":62},[172,173,176],"span",{"class":174,"line":175},"line",1,[172,177,179],{"class":178},"sxvE3","// Before: 同期で値を取得\n",[172,181,183,187,191,195],{"class":174,"line":182},2,[172,184,186],{"class":185},"stQ0i","function",[172,188,190],{"class":189},"senZ8"," getServiceName",[172,192,194],{"class":193},"shFtX","()",[172,196,197],{"class":193}," {\n",[172,199,201,205,209,212,215,218,222,226,228,231,234,237,240,242],{"class":174,"line":200},3,[172,202,204],{"class":203},"sHkkW","  return",[172,206,208],{"class":207},"s4oTP"," sessionStorage",[172,210,211],{"class":193},".",[172,213,214],{"class":189},"getItem",[172,216,217],{"class":193},"(",[172,219,221],{"class":220},"sMJiu","'",[172,223,225],{"class":224},"sdGka","serviceName",[172,227,221],{"class":220},[172,229,230],{"class":193},")",[172,232,233],{"class":185}," ||",[172,235,236],{"class":220}," '",[172,238,239],{"class":224},"default",[172,241,221],{"class":220},[172,243,244],{"class":193},";\n",[172,246,248],{"class":174,"line":247},4,[172,249,250],{"class":193},"}\n",[172,252,254],{"class":174,"line":253},5,[172,255,257],{"emptyLinePlaceholder":256},true,"\n",[172,259,261],{"class":174,"line":260},6,[172,262,263],{"class":178},"// After: chrome.storage.local は非同期\n",[172,265,267,270,273,275,277],{"class":174,"line":266},7,[172,268,269],{"class":185},"async",[172,271,272],{"class":185}," function",[172,274,190],{"class":189},[172,276,194],{"class":193},[172,278,197],{"class":193},[172,280,282,285,288,291,294,297,299,302,304,307,309,312,314,316,318,320],{"class":174,"line":281},8,[172,283,284],{"class":185},"  const",[172,286,287],{"class":207}," result",[172,289,290],{"class":193}," =",[172,292,293],{"class":203}," await",[172,295,296],{"class":207}," chrome",[172,298,211],{"class":193},[172,300,301],{"class":207},"storage",[172,303,211],{"class":193},[172,305,306],{"class":207},"local",[172,308,211],{"class":193},[172,310,311],{"class":189},"get",[172,313,217],{"class":193},[172,315,221],{"class":220},[172,317,225],{"class":224},[172,319,221],{"class":220},[172,321,322],{"class":193},");\n",[172,324,326,328,330,332,334,336,338,340,342],{"class":174,"line":325},9,[172,327,204],{"class":203},[172,329,287],{"class":207},[172,331,211],{"class":193},[172,333,225],{"class":207},[172,335,233],{"class":185},[172,337,236],{"class":220},[172,339,239],{"class":224},[172,341,221],{"class":220},[172,343,244],{"class":193},[172,345,347],{"class":174,"line":346},10,[172,348,250],{"class":193},[15,350,351,352,355],{},"呼び出し元を確認したところ、すべてasync関数の内部から呼ばれていた。",[35,353,354],{},"await getServiceName()","に書き換えるだけで済んだ。呼び出しチェーンを遡って同期→非同期の連鎖書き換えが発生する最悪のパターンにはならなかった。",[27,357,358],{"id":358},"テストのモック定義を更新",[15,360,361,362,364,365,367,368,371],{},"テストでは",[35,363,80],{},"のモックを",[35,366,51],{},"のモックに差し替えた。",[35,369,370],{},"chrome.storage.local.get","がPromiseを返すように定義し直す。",[54,373,375],{"className":166,"code":374,"language":168,"meta":62,"style":62},"// Before\nglobal.sessionStorage = {\n  getItem: vi.fn(),\n  setItem: vi.fn(),\n};\n\n// After\nglobal.chrome = {\n  storage: {\n    local: {\n      get: vi.fn().mockResolvedValue({}),\n      set: vi.fn().mockResolvedValue(undefined),\n    },\n  },\n};\n",[35,376,377,382,395,415,430,435,439,444,457,466,475,498,524,530,536],{"__ignoreMap":62},[172,378,379],{"class":174,"line":175},[172,380,381],{"class":178},"// Before\n",[172,383,384,387,389,391,393],{"class":174,"line":182},[172,385,386],{"class":207},"global",[172,388,211],{"class":193},[172,390,80],{"class":207},[172,392,290],{"class":193},[172,394,197],{"class":193},[172,396,397,401,404,407,409,412],{"class":174,"line":200},[172,398,400],{"class":399},"sz8Xr","  getItem",[172,402,403],{"class":193},":",[172,405,406],{"class":207}," vi",[172,408,211],{"class":193},[172,410,411],{"class":189},"fn",[172,413,414],{"class":193},"(),\n",[172,416,417,420,422,424,426,428],{"class":174,"line":247},[172,418,419],{"class":399},"  setItem",[172,421,403],{"class":193},[172,423,406],{"class":207},[172,425,211],{"class":193},[172,427,411],{"class":189},[172,429,414],{"class":193},[172,431,432],{"class":174,"line":253},[172,433,434],{"class":193},"};\n",[172,436,437],{"class":174,"line":260},[172,438,257],{"emptyLinePlaceholder":256},[172,440,441],{"class":174,"line":266},[172,442,443],{"class":178},"// After\n",[172,445,446,448,450,453,455],{"class":174,"line":281},[172,447,386],{"class":207},[172,449,211],{"class":193},[172,451,452],{"class":207},"chrome",[172,454,290],{"class":193},[172,456,197],{"class":193},[172,458,459,462,464],{"class":174,"line":325},[172,460,461],{"class":399},"  storage",[172,463,403],{"class":193},[172,465,197],{"class":193},[172,467,468,471,473],{"class":174,"line":346},[172,469,470],{"class":399},"    local",[172,472,403],{"class":193},[172,474,197],{"class":193},[172,476,478,481,483,485,487,489,492,495],{"class":174,"line":477},11,[172,479,480],{"class":399},"      get",[172,482,403],{"class":193},[172,484,406],{"class":207},[172,486,211],{"class":193},[172,488,411],{"class":189},[172,490,491],{"class":193},"().",[172,493,494],{"class":189},"mockResolvedValue",[172,496,497],{"class":193},"({}),\n",[172,499,501,504,506,508,510,512,514,516,518,521],{"class":174,"line":500},12,[172,502,503],{"class":399},"      set",[172,505,403],{"class":193},[172,507,406],{"class":207},[172,509,211],{"class":193},[172,511,411],{"class":189},[172,513,491],{"class":193},[172,515,494],{"class":189},[172,517,217],{"class":193},[172,519,520],{"class":185},"undefined",[172,522,523],{"class":193},"),\n",[172,525,527],{"class":174,"line":526},13,[172,528,529],{"class":193},"    },\n",[172,531,533],{"class":174,"line":532},14,[172,534,535],{"class":193},"  },\n",[172,537,539],{"class":174,"line":538},15,[172,540,434],{"class":193},[15,542,543],{},"修正後、全135テストを走らせた。すべてグリーン。",[19,545],{},[22,547,549],{"id":548},"デッドコードの発見-setexportqueue","デッドコードの発見: setExportQueue",[15,551,552,553,556],{},"移行作業でコードを読み歩いていたら、",[35,554,555],{},"setExportQueue","という関数が目に止まった。storage.jsで定義されてエクスポートもされているが、プロダクションコードから一度も呼ばれていない。テストコードからだけ参照されていた。",[15,558,559],{},"grepで呼び出し元を全件洗い出して確認した。import文を含めてもテストファイルしかヒットしない。おそらく初期の設計段階で作られ、実装が進む中で別のアプローチに切り替わったが、関数定義だけが残ったのだろう。",[15,561,562],{},"今回は削除せずコメントで「未使用」と注記だけ残した。次のリファクタリングで消す。",[19,564],{},[22,566,568],{"id":567},"twitterアーカイブ-sqlite取り込み","Twitterアーカイブ → SQLite取り込み",[27,570,572],{"id":571},"_16gbのzipを展開する","1.6GBのZIPを展開する",[15,574,575,576,579],{},"同日、Twitterから届いたアーカイブファイル（約1.6GB）の処理にも着手した。ZIPを展開すると、ツイートデータがJavaScript形式（",[35,577,578],{},"window.YTD.tweet.part0 = [...]","）で格納されている。先頭の代入文を削ってJSONとしてパースする、いつものTwitterアーカイブ処理パターンを踏んだ。",[27,581,583],{"id":582},"_11669件のツイートをsqliteに格納","11,669件のツイートをSQLiteに格納",[15,585,586],{},"パースしたJSONから各ツイートのID、投稿日時、本文、リプライ先、リツイート元などを抽出し、SQLiteのテーブルに流し込んだ。",[54,588,592],{"className":589,"code":590,"language":591,"meta":62,"style":62},"language-sql shiki shiki-themes vitesse-light vitesse-light","CREATE TABLE tweets (\n  id TEXT PRIMARY KEY,\n  created_at TEXT,\n  full_text TEXT,\n  in_reply_to_status_id TEXT,\n  retweet_count INTEGER,\n  favorite_count INTEGER\n);\n","sql",[35,593,594,609,623,632,641,650,660,668],{"__ignoreMap":62},[172,595,596,599,602,605],{"class":174,"line":175},[172,597,598],{"class":203},"CREATE",[172,600,601],{"class":203}," TABLE",[172,603,604],{"class":189}," tweets",[172,606,608],{"class":607},"sG7-3"," (\n",[172,610,611,614,617,620],{"class":174,"line":182},[172,612,613],{"class":607},"  id ",[172,615,616],{"class":185},"TEXT",[172,618,619],{"class":185}," PRIMARY KEY",[172,621,622],{"class":607},",\n",[172,624,625,628,630],{"class":174,"line":200},[172,626,627],{"class":607},"  created_at ",[172,629,616],{"class":185},[172,631,622],{"class":607},[172,633,634,637,639],{"class":174,"line":247},[172,635,636],{"class":607},"  full_text ",[172,638,616],{"class":185},[172,640,622],{"class":607},[172,642,643,646,648],{"class":174,"line":253},[172,644,645],{"class":607},"  in_reply_to_status_id ",[172,647,616],{"class":185},[172,649,622],{"class":607},[172,651,652,655,658],{"class":174,"line":260},[172,653,654],{"class":607},"  retweet_count ",[172,656,657],{"class":185},"INTEGER",[172,659,622],{"class":607},[172,661,662,665],{"class":174,"line":266},[172,663,664],{"class":607},"  favorite_count ",[172,666,667],{"class":185},"INTEGER\n",[172,669,670],{"class":174,"line":281},[172,671,322],{"class":607},[15,673,674],{},"11,669件のINSERTが数秒で完了した。SQLiteにデータが入ると、年度別・月別のツイート数やリプライ率をSQLで一発で引ける。",[27,676,677],{"id":677},"年度別サマリーの生成",[15,679,680],{},"SQLiteに入ったデータから年度別の投稿数をCOUNT + GROUP BYで集計し、マークダウンのサマリーファイルを生成した。投稿頻度が年ごとにどう変化したかが一覧で見える形になった。",[15,682,683],{},"前日に取り込んだXブックマーク8,251件と合わせて、Twitterでの活動データがほぼすべてローカルのSQLiteに集約された。",[19,685],{},[22,687,688],{"id":688},"今日の学び",[690,691,692,700,706,712],"ul",{},[693,694,695,699],"li",{},[696,697,698],"strong",{},"sessionStorageは拡張間で丸見え","。content scriptが注入されるページのWeb Storage APIは全拡張で共有される。chrome.storage.localを使えば拡張ごとに隔離される",[693,701,702,705],{},[696,703,704],{},"同期→非同期の変更は呼び出し元から確認","。今回はすべてasync関数内だったので影響が限定的だった。呼び出しチェーンを先に洗い出す手順が被害範囲の見積もりに直結する",[693,707,708,711],{},[696,709,710],{},"grepで歩くとデッドコードが見つかる","。目的外の発見がリファクタリングの種になる",[693,713,714,717],{},[696,715,716],{},"Twitterアーカイブは先頭の代入文を削ればJSON","。この変換パターンを覚えておけば、次回のアーカイブ取り込みは手が迷わない",[719,720,721],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}",{"title":62,"searchDepth":182,"depth":182,"links":723},[724,728,733,734,739],{"id":24,"depth":182,"text":25,"children":725},[726,727],{"id":29,"depth":200,"text":30},{"id":44,"depth":200,"text":45},{"id":70,"depth":182,"text":71,"children":729},[730,731,732],{"id":74,"depth":200,"text":74},{"id":144,"depth":200,"text":145},{"id":358,"depth":200,"text":358},{"id":548,"depth":182,"text":549},{"id":567,"depth":182,"text":568,"children":735},[736,737,738],{"id":571,"depth":200,"text":572},{"id":582,"depth":200,"text":583},{"id":677,"depth":200,"text":677},{"id":688,"depth":182,"text":688},"dev","sessionStorageが他の拡張からも読める問題を発見し、chrome.storage.localへ全面移行。あわせて約1.6GBのTwitterアーカイブをSQLiteに取り込み、年度別サマリーを生成した作業ログ","md",{},"/chrome-extension-storage-security","chrome-extension-x",false,"2026-04-07T00:00:00.000Z",{"title":5,"description":741},"2026-04/2026-04-07/chrome-extension-storage-security",[751,752,80,51,753,754],"Chrome拡張機能","セキュリティ","Twitter","SQLite","memo",null,"Dp_wnGNwNZjVMfplrB-1A0yO3og4q77iOWLbyJI0eVs",[],"https://log.eurekapu.com/favicon.svg",1775602359766]