Babel
イベント
該当するコンテンツが見つかりませんでした
マガジン
該当するコンテンツが見つかりませんでした
技術ブログ
この記事は KINTO テクノロジーズ Advent Calendar 2025 の 25 日目の記事です 🎅🎄 はじめに こんにちは! KINTO 開発部 KINTO バックエンド開発 G マスターメンテナンスツール開発チーム、技術広報 G 兼務、Osaka Tech Lab 所属の high-g( @high_g_engineer )です。フロントエンドエンジニアをやっています。 現在開発中のプロジェクトでは、RC 版の頃から React Compiler を導入しており、約 8 ヶ月が経ちました。 導入によって useMemo や useCallback を書かなくても自動でメモ化されるため、メモ化を意識する必要がなくなり、開発体験は向上しました。 しかし、実際にどの程度メモ化が正しく行われているのか、パフォーマンスにどれくらいの影響があるのかは、詳しく検証できていませんでした。 そこで本記事では、React Compiler の有効時の挙動や、有効時と無効時のパフォーマンス比較を検証してみることにしました。 React Compiler とは https://ja.react.dev/learn/react-compiler/introduction React Compiler は、ビルド時に自動的にメモ化を行うことで React アプリを最適化するツールです。 そのため、React Compiler を導入すれば、 useMemo や useCallback 、 React.memo などを手動で書く必要がなくなります。 最初の安定版(v1.0)は 2025 年 10 月 7 日にリリースされ、この記事が執筆された時点で約 2 か月半が経過しています。 安定版リリースまでの経緯は以下の通りです。 2023 年 3 月 - 「React Forget」として開発、Meta 社内の限定的な領域で production 利用開始 2023 年 10 月 - React Advanced Conference 2023 で「React Forget」として公開発表 2024 年 2 月 - instagram 全体で production 展開完了、Meta 社内の他サービスへ展開、OSS 化準備と発表 2024 年 5 月 - React Conf 2024 で experimental release を発表 2024 年 10 月 21 日 - Beta release を公開 2025 年 4 月 21 日 - Release Candidate (RC) を公開 2025 年 10 月 7 日 - v1.0 安定版リリース React Compiler の設定方法 Vite と React 19 を使用した環境での React Compiler の設定方法を紹介します。 1. パッケージのインストール pnpm add -D babel-plugin-react-compiler 2. vite.config.ts の設定 @vitejs/plugin-react の babel オプションに babel-plugin-react-compiler を追加します。 import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; // 設定オプション const ReactCompilerConfig = { /* ... */ }; export default defineConfig({ plugins: [ react({ babel: { plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]], }, }), ], }); これだけで設定は完了です。後はビルド時に React Compiler が自動的にコードを解析し、必要な箇所にメモ化を適用してくれます。 React Compiler の設定オプションに関しては、本記事では説明を割愛します。 特に設定しなくても動作しますが、詳細を知りたい方は以下を参照してください。 https://ja.react.dev/reference/react-compiler/configuration ベンチマーク対象の React アプリ 実際の開発プロジェクトで検証を試みましたが、使用しているライブラリに既にメモ化されたコンポーネントが多く含まれていて、純粋な比較が困難だったので、ベンチマーク専用のプロジェクトを作成しました。 ヘッダー、サイドメニュー、メインコンテンツのエリアで構成され、初期表示時の合計コンポーネント数は約 100 個です。 最初に、React Compiler が無効の状態で、React Dev Tools でメモ化の状況を確認していきます。 Components タブを見ると、メモ化されている場合に表示される「Memo」のラベルが一切ないことがわかります。 次に、React Dev Tools の設定で Highlight updates when components render にチェックを入れて、上位にあるボタンコンポーネントをクリックし再レンダリングの様子を確認すると、本来再レンダリングが不要な子孫コンポーネントにも再レンダリングが発生していることが分かります。 React Compiler のメモ化の挙動 React Compiler を有効にして開発サーバーを立ち上げ、同じアプリがメモ化されているかを確認します。 React Dev Tools の Components タブを確認すると、「Memo」のラベルが数多く表示されていることが分かります。 同様に、上位にあるボタンコンポーネントをクリックし、React Dev Tools で再レンダリングの状態を確認すると、再レンダリングが必要最小限に抑えられていることが分かります。 React Compiler のパフォーマンスベンチマーク ここからは、React Compiler によるパフォーマンスの差を React Dev Tools の Profiler を用いて検証していきます。 検証では、下記の赤枠のボタンを約 1 秒間隔で 10 回連続クリックした際の、レンダリング時間の比較を行いました。 このボタンはメインコンテンツの最上位に配置されているため、クリック時に多くの子孫コンポーネントへ再レンダリングの影響が波及します。 React Compiler 無効時(メモ化なし) 計測データ 1回目: 29ms 2回目: 34.5ms 3回目: 36.1ms 4回目: 33.9ms 5回目: 36.3ms 6回目: 17.6ms 7回目: 35.1ms 8回目: 32.1ms 9回目: 33.3ms 10回目: 36.8ms 平均レンダリング時間 32.5ms Flamegraph を見ると、すべての子孫コンポーネントがレンダリングされていることが分かります。また、MainContents 以外にもレンダリング時間が長いコンポーネント(黄色やオレンジ色で表示)が存在し、本来不要な再レンダリングが発生していることが確認できます。 React Compiler 有効時(メモ化あり) 計測データ 1回目: 11.1ms 2回目: 12.1ms 3回目: 12.2ms 4回目: 12.1ms 5回目: 12.1ms 6回目: 12.1ms 7回目: 12.1ms 8回目: 12.0ms 9回目: 12.0ms 10回目: 12.6ms 平均レンダリング時間 12.0ms Flamegraph を見ると、グレーの斜線で表示されているコンポーネントが多数確認でき、再レンダリングが必要最小限に抑えられていることが分かります。 パフォーマンス改善の結果 React Compiler 無効時 React Compiler 有効時 改善結果 平均レンダリング時間 32.5ms 12.0ms 約 2.7 倍高速化 React Compiler を有効にしただけで、非常に大きなパフォーマンス改善ができていることが確認できました。 今回はメモ化を全く行っていないプロジェクトとの比較のため、すべてのケースで同様の改善が見込めるわけではありませんが、導入効果は十分に期待できます。 懸念点 ライブラリとの相性 現在開発中のプロジェクトでは TanStack Table を利用しています。TanStack Table は新しい参照を意図的に生成することで再レンダリングをトリガーする設計のため、React Compiler によるメモ化が適用されると、再レンダリングが発生せず意図しない挙動を引き起こす可能性があります。 この問題に対しては、 "use no memo" ディレクティブを追加して部分的に React Compiler を無効化することで対応が可能です。 function TableComponent() { "use no memo"; const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), }); return ( <table> {/* TanStack Table の参照変化に依存する処理 */} </table> ); } react-hook-form など、同様に参照の変化に依存するライブラリを利用する場合は、要所で "use no memo" の記述が必要になるため、導入時にはご注意ください。 ビルド速度低下、ビルドファイルサイズ上昇 React Compiler を導入すると、再レンダリング時のパフォーマンスは向上しますが、メモ化のためのコードが追加されるため、ビルド速度やファイルサイズに多少影響が出ます。その結果、初回読み込みが少し遅くなる可能性があります。 React Compiler 無効時のビルド結果 ビルド時間: 64ms ファイルサイズ: 232KB React Compiler 有効時のビルド結果 ビルド時間: 556ms ファイルサイズ: 248KB まとめ 今回は React Compiler を有効にしたときのメモ化に関する挙動の確認と、パフォーマンスにどれくらい影響があるのかを検証してみました。 結果として、メモ化が正しく動作し、そのおかげでパフォーマンス改善も十分に期待できることが分かりました。 ただし、いくつか注意点もあります。 参照の変化に依存するライブラリを使用する場合は、 "use no memo" で部分的に無効化が必要になることがあります。また、メモ化のコードが追加される分、ビルド速度やファイルサイズには多少影響が出ます。 とはいえ、これらは対処可能な範囲ですし、パフォーマンス改善だけでなくメモ化を意識しなくて済むというメリットは非常に大きいです。React Compiler の導入を迷われている方は、ぜひ試してみてください。 最後まで読んでいただきありがとうございました。 参考記事 https://ja.react.dev/learn/react-compiler/introduction https://ja.react.dev/reference/react-compiler/configuration https://reactadvanced.com/2023/ https://conf2024.react.dev/ https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023 https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024 https://react.dev/blog/2025/10/07/react-compiler-1 https://github.com/TanStack/table/issues/5567
はじめに この記事はBASEアドベントカレンダー2025の16日目の記事です。 こんにちは。Pay ID プラットフォーム Group で エンジニアをしている noji です。最近は Pay ID の認証基盤のフロントエンド開発を担当しています。 本記事では BASE のショップや Pay ID アプリでの買い物時にカートでの Pay ID ログイン機能を提供している JavaScript(以後 payid-js)のビルド環境を webpack/Babel から esbuild に移行した話を紹介します。 payid-js について payid-js は Pay ID ログイン機能を提供している埋め込み用の JavaScript です。Pay ID ログインすることで、Pay ID に登録されている住所情報や決済手段情報を連携することで、ユーザーはスムーズに購入手続きを進めることができます。 BASE のカートのフロントエンドで payid-js を読み込み、用意された関数を呼び出すと画面上にログイン用の画面が iframe 上に表示され、Pay ID にログインできます。iframe 内でログイン処理を行い、結果を postMessage API を使ってカートのフロントエンドに通知します。 payid-js は iframe 内外でやり取りを行うインターフェースを提供しており、iframe 内でログインが完了すると、結果をカートのフロントエンドに返すようになっています。(iframe の内側の画面については別システム) 技術としては、TypeScript で実装されており、ビルドには webpack と Babel を使用していました。 移行背景 payid-js は BASE のカートと iframe で表示される Pay ID ログイン画面の橋渡しをするだけのコンポーネントなので、軽量な JavaScript です。 軽量であるので、webpack のビルドに時間がかかるとは感じていませんでしたが webpack 時代のバージョンアップや設定変更が大変 Babel を含む関連ライブラリの設定が複雑 依存関係の脆弱性が多い payid-js には webpack ほど高度な機能が不要である などの課題があり、よりシンプルなビルドツールへの移行を検討しました。 esbuild を選んだ理由 候補として esbuild、vite、Rollup などがありましたが、最終的に esbuild を選択しました。理由は以下です。 シンプルで高速なビルドが可能 Go 製でビルドが非常に速いのに加えて、設定もシンプルでわかりやすく、TypeScriptのトランスパイルも内蔵されていてBabelも不要 依存関係が少なく、メンテナンスコストや脆弱性リスクが低い webpack や Babel に比べて関連ライブラリ等の依存関係が少なく、アップデートや脆弱性の対応に追われる負荷が軽減されそう 他ツールとの比較 vite:SPA 向けの開発サーバーは強力だけど、payid-js のような埋め込み用 JavaScript にはオーバースペック Rollup:esbuild ほど高速ではなく、設定もやや複雑になる。ライブラリ向けには良いが、今回は見送り esbuild は HMR(Hot Module Replacement) をサポートしていないですが、payid-js は埋め込み用の JavaScript であり、開発時に HMR は必要ないため問題ありませんでした。 参考 esbuild vite Rollup 移行で詰まったポイント ローカルの 開発サーバーの構築 今までは webpack-dev-server を使用してローカル開発環境を構築していました。 webpack-dev-server はビルドしたアセットをメモリに保持し、変更があれば自動で配信内容を更新してくれる開発サーバーを内蔵しています。 Docker からのアクセスでも常に最新が返ってくるため、ビルド・配信・更新反映をひとまとめに解決してくれる優れた仕組みでした。 一方、esbuildにはwebpack-dev-serverのような開発サーバーは内蔵されておらず、あくまで”ビルド”のみの機能です。今回は serve で簡易的に http-server を立ち上げるスクリプトを用意しました。watch だけだと変更を検知して Docker コンテナに反映させることができなかったので、 chokidar も利用し、変更を検知して明示的に再ビルドできるようにしました。 #!/usr/bin/env node import path from "path" ; import { fileURLToPath } from "url" ; import * as esbuild from "esbuild" ; import { spawn } from "child_process" ; import chokidar from "chokidar" ; const __filename = fileURLToPath ( import . meta . url ) ; const __dirname = path . dirname ( __filename ) ; const outdir = path . resolve ( __dirname , "dist" ) ; // esbuild の watch 用コンテキストを作成 const ctx = await esbuild . context ({ entryPoints : [ path . resolve ( __dirname , "src" , "index.ts" )] , bundle : true , sourcemap : true , platform : "browser" , outdir , entryNames : "bundle" , minify : true , loader : { ".html" : "text" , } , }) ; await ctx . watch () ; console . log ( "esbuild: watching" , outdir ) ; // chokidar でファイル変更を監視して rebuild const watcher = chokidar . watch ([ path . resolve ( __dirname , "src" )] , { ignoreInitial : true , usePolling : true , interval : 100 , }) ; let rebuilding = false ; async function scheduleRebuild ( event , filePath ) { if ( rebuilding ) return; rebuilding = true ; console . log ( `change detected ( ${ event } ):` , filePath ) ; try { await ctx . rebuild () ; console . log ( "esbuild: rebuild complete" ) ; } finally { setTimeout (() => ( rebuilding = false ) , 50 ) ; } } watcher . on ( "add" , ( p ) => scheduleRebuild ( "add" , p )) ; watcher . on ( "change" , ( p ) => scheduleRebuild ( "change" , p )) ; watcher . on ( "unlink" , ( p ) => scheduleRebuild ( "unlink" , p )) ; // ローカルサーバー (npx serve) spawn ( "npx" , [ "serve" , "-s" , outdir , "-l" , "9000" ] , { stdio : "inherit" , shell : true , }) ; このスクリプトを実行すると、chokidar がソースコードの変更を監視し、変更があった場合に再ビルドを行います。また、 npx serve を使用してローカルサーバーを立ち上げ、ブラウザから埋め込み用 JavaScript を確認できるようにしています。 ビルドの成果物の違い 基本的に成果物はほぼ同じでしたが、loaderの指定によりHTML の import 部分で差異がありました。 webpack: HTML モジュールをオブジェクトとして扱う esbuild: HTML モジュールを文字列として扱う そのため後々の移行の手順にもあるように、一定期間同じコードベースで webpack/esbuild の両方をビルドする必要があったため、どちらのビルド方法でも動作するように、下記のようなユーティリティ関数を追加しました。 const rawModule = require( "./container.html" ); const html = getHtmlStringFromModule(rawModule); // `*.html` をバンドルする方法はバンドラによって異なります。 // - esbuild や rollup の一部設定では、インポートはそのまま文字列になります。 // - もしくは `{ default: string }` のようなオブジェクトを返す場合もあります。 // ここで形を正規化することで常に文字列として扱えるようにします。 const getHtmlStringFromModule = ( mod : unknown ): string => { if ( typeof mod === "string" ) { return mod; } if ( typeof mod === "object" && mod !== null ) { const maybeDefault = (mod as Record < string , unknown >). default ; if ( typeof maybeDefault === "string" ) { return maybeDefault; } } throw new Error ( "unexpected HTML module shape" ); } ; ビルド用の設定 #!/usr/bin/env node import path from "path" ; import { fileURLToPath } from "url" ; import { build } from "esbuild" ; const __filename = fileURLToPath ( import . meta . url ) ; const __dirname = path . dirname ( __filename ) ; const outdir = path . resolve ( __dirname , "dist" ) ; const outfile = path . join ( outdir , "bundle.js" ) ; // 本番ビルド await build ({ entryPoints : [ path . resolve ( __dirname , "src" , "index.ts" )] , bundle : true , sourcemap : true , platform : "browser" , outfile , minify : true , define : { API_BASE_URL : JSON . stringify ( process . env . API_BASE_URL || "" ) , } , loader : { ".html" : "text" , } , logLevel : "info" , }) ; console . log ( "esbuild: built" , outfile ) ; ビルド用のスクリプトも非常にシンプルです。esbuild の build 関数を使用して、エントリーポイントや出力先、バンドル設定などを指定しています。 ビルドされたファイルを CircleCI のジョブで S3 にアップロードし、CDN 経由で配信する仕組みは以前と同様に維持しています。 移行の手順 移行は段階的に行いました。 ローカル/dev 環境のみ esbuild に切り替え stg/本番も esbuild に切り替え webpack/Babel 関連の設定・依存関係を削除 結果として、問題なく移行でき、切り替えによる影響もありませんでした。 移行結果 元々軽量な JavaScript であったため、ビルド時間の劇的な改善はありませんでしたが、設定が大幅にシンプルになり、依存関係の脆弱性も出にくくなりました。 元々が CircleCI 上で 2 ~3秒程度のビルド時間でしたが、esbuild に移行したことで 1 秒未満に短縮されました!! おわりに payid-js のビルドを webpack/Babel から esbuild に移行したことで、設定のシンプル化と依存関係の削減が実現できました。 今後も payid-js の開発を続けていく中で、さらなる改善点が見つかれば積極的に取り組んでいきたいと思います。 BASE / Pay IDではエンジニアを募集しているので、興味ある方は以下からご連絡ください。 明日のBASEアドベントカレンダーはIzuharaさんの記事です。お楽しみに。 binc.jp
こんにちは。メルペイのPayment & Customer PlatformのAccountingチームでBackend Engineerをしている @mewuto (みゅーと)です。 この記事は、 Merpay & Mercoin Advent Calendar 2025 の13日目の記事です。 要旨 メルカリでは、業務自動化プラットフォームとして n8n を導入していますが、ワークフローの自由度が高い反面、セキュリティリスクも懸念されます。特に「権限混同(Confused Deputy Problem)」は重大な脅威であり、個人の権限を不適切に共有することで、意図しないデータアクセスや操作が可能になってしまいます。 本記事では、n8nワークフローのセキュリティレビューを自動化するCLIツールの開発と、GitHub Actionsを活用した実際の運用例について解説します。このツールは GitHub でOSS公開しており、 npm からインストール可能です。 対象読者 : ワークフロー自動化ツール(n8n、Zapier等)を使用している開発者・セキュリティ担当者 JSONベースの静的解析やDAGを用いたフロー解析に興味のある方 業務自動化基盤のセキュリティ強化に取り組んでいる方 目次 背景:n8n導入とセキュリティ課題 セキュリティポリシーの定義 アーキテクチャ概要 ノードレベル検出の実装例:BigQuery本番環境アクセス検知 シナリオレベル検出の実装例 検出の例および社内での活用事例 成果と今後の展望 まとめ 1. 背景:n8n導入とセキュリティ課題 n8nとは n8n は、ノーコード/ローコードで業務自動化ワークフローを構築できるオープンソースのプラットフォームです。ZapierやMakeと同様に、異なるサービス間のデータ連携や処理の自動化が可能ですが、n8nはより自由度が高く、柔軟なカスタマイズや複雑なフローの構築ができる点が特徴です。 メルカリでは、開発者やビジネスチームの業務効率化のため、n8n Enterpriseを導入しました(参考: 理想の Workflow Platform という“聖杯”に、n8n でついに手が届くかもしれない )。しかし、その自由度の高さゆえに、以下のセキュリティ課題が浮上しました。 Confused Deputy Problem(権限混同)の脅威 「Confused Deputy Problem」とはあるシステムにおいて、ユーザーが本来持つ権限よりも大きな権限が設定されているときに、アクセスできないはずのリソースにアクセスできたり変更できてしまう問題です。n8nワークフローでは、権限を持つメンバーのcredentialが組み込まれているため、以下のようなリスクが発生します。 データベースへの意図しないアクセス : 本来DBアクセス権を持たないメンバーが、Slackボットを実行することで、権限を持つメンバーのcredentialを通じて本番データベースにアクセスできてしまう 機密情報の意図しない漏洩 : ワークフローが出力した機密情報(スプレッドシート、Slack Publicチャンネルへの投稿等)を、本来アクセス権を持たないメンバーが閲覧できてしまう チーム間での権限境界の破綻 : 別チームのメンバーが作成したワークフローを実行することで、本来アクセスできない他チームのリソース(社内ドキュメント、ストレージなど)にアクセスできてしまう これらの問題を防ぐため、ワークフロー有効化前の手動レビューを実施していましたが、以下の課題がありました。 レビュー工数の増大と品質の不安定性 : 1ワークフローあたり、修正依頼や修正後の再レビューを含めて30分〜1時間のレビュー時間が必要。また、セキュリティガイドラインは整備されているものの、詳細な技術ドキュメントを参照しながらワークフローを作成することは容易ではなく、ユーザー自身での自己改善が難しい。加えて、人間によるレビューである以上、見落としのリスクも存在する 継続的監視の欠如 : 承認後もワークフローは自由に変更可能であり、変更後の安全性を保証できない これらの課題を解決するため、静的セキュリティ解析ツールの開発に至りました。 2. セキュリティポリシーの定義 ツール開発にあたり、セキュリティガイドラインを策定しました。主要な要件は以下の通りです。 2.1 権限混同の防止 入力ソースの制限 : ワークフローをトリガーできるユーザーを明示的に制限 出力先の制限 : 結果の出力先を閲覧権限を持つユーザー内に限定 2.2 Slack Bot特有の要件 実行可能メンバーのホワイトリスト化 : Slackトリガーには必ずユーザー検証を実装 エフェメラルメッセージの使用 : 機密情報は一時的なメッセージで返す Private Channel推奨 : Public Channelへの出力を制限 2.3 データの入出力制御 本番環境アクセスの明示 : 本番データへのアクセスは明示的に警告 動的クエリの検証 : 外部入力による動的クエリを検出。動的にSQLクエリを構築すると、本来意図していないテーブルやデータセットにアクセスできてしまうリスクがあるため、最小権限の原則に基づき使用範囲を制限 入力値のフィルタリング : 外部入力を使用する箇所での検証実装 出力先のアクセス権限制御 : Google Sheetsなどへの出力時は、適切なアクセス権限スコープ(閲覧可能ユーザーの制限)が設定されていることを確認 これらのポリシーを自動検証することで、手動レビューの負担を大幅に軽減できます。 3. アーキテクチャ概要 3.1 全体構成と処理の流れ ツールは以下の処理フローで動作します。 n8nワークフローJSON読み込み 静的解析による検証 : 2.1. ノードレベルチェック : 個別ノード(BigQuery、HTTP Request、JavaScript Code、Google Sheets/Drive等)の設定を検証 2.2 シナリオレベルチェック : 複数ノード間の関係性(Slackトリガー後の呼び出しユーザーのバリデーション実装、スプレッドシート出力先の権限範囲等)をDAG(有向非巡環グラフ)を用いて解析。 ※ なぜDAGが必要か : LLMによるチェックは再現性が保証されず、論理的なグラフ解析もまだ未発達です。DAGによる静的解析は、ノード間の実行順序や依存関係を確実に追跡でき、「バリデーション → 外部アクセス」といったセキュリティ上重要な順序関係を100%の精度で検証可能です。 検出結果の出力 : Console、JSON、PR Comment形式で結果を出力 3.2 ワークフローJSONの構造 n8nのワークフローは、以下のようなJSON形式で保存されます。 { "name": "My workflow", "nodes": [ { "id": "node-id-1", "type": "n8n-nodes-base.googleBigQuery", "parameters": { "projectId": "merpay-prod", "sqlQuery": "{{ $json.query }}", "operation": "executeQuery" } }, { "id": "node-id-2", "type": "n8n-nodes-base.code", "parameters": { "jsCode": "const data = $input.item.json;\nreturn { result: data.rows.length };" } }, { "id": "node-id-3", "type": "n8n-nodes-base.slack", "parameters": { "resource": "message", "operation": "post", "channel": "#general", "text": "{{ $json.result }}" } } ], "connections": { "node-id-1": { "main": [[{"node": "node-id-2", "type": "main", "index": 0}]] }, "node-id-2": { "main": [[{"node": "node-id-3", "type": "main", "index": 0}]] } } } このJSONから以下の情報を抽出できます。 ノード設定 : 各ノードのtype、parameters、credentials フロー構造 : connectionsによるノード間の依存関係 各ノードのparametersには、ノードタイプに応じた詳細情報が含まれます。 JavaScript Codeノードの実装内容 BigQueryのデータセット名やSQLクエリ HTTPリクエストのエンドポイント、メソッド、リクエストボディ この構造化された豊富な情報により、実行前の詳細な静的解析が可能になります。 3.3 2層検出アーキテクチャ セキュリティリスクは、個別ノードの設定だけでなく、ノード間の関係性によっても発生します。そのため、本ツールでは以下の2層アーキテクチャを採用しています。 ノードレベル検出 : 個別ノードの設定を検証 例:BigQueryの本番プロジェクトアクセス、Slackの投稿先チャンネル 各ノードが独立して安全な設定になっているかを確認 シナリオレベル検出 : 複数ノード間の関係性を検証 DAGを用いたフロー解析 例: (i) Slack Trigger → ユーザーバリデーション → 外部アクセスの順序検証 (ii) Google Sheets Create → Google Drive Share/HTTP Request Permissionsのスコープ設定検証 ワークフロー全体としてセキュリティ要件を満たしているかを確認 この2層アプローチにより、個別の設定ミスだけでなく、ワークフロー全体の設計上の問題も検出できます。 4. ノードレベル検出の実装例:BigQuery本番環境アクセス検知 ノードレベル検出の具体例として、BigQueryの本番環境アクセス検知を解説します。 4.1 検出対象 以下のようなワークフローを検出します。 { "parameters": { "projectId": "merpay-prod", "operation": "executeQuery" }, "type": "n8n-nodes-base.googleBigQuery" } この設定では、 projectId に prod 文字列を含む本番環境プロジェクトへのアクセスを検出します。 4.2 実装 検出ロジックは BigQueryChecker に実装されています。 // Typescript // パラメータからプロジェクトIDを取得 const projectId = node.parameters.projectId; // 本番環境へのアクセスを検出 if (projectId.includes('prod')) { // 警告を追加 addWarning('本番環境へのアクセスが検出されました'); } 詳細な実装は GitHubリポジトリ を参照してください。 4.3 検出結果 検出された問題は以下のような形式で報告されます。 ### ⚠️ 警告 (Warnings) (1) - **[Execute a SQL query]** (n8n-nodes-base.googleBigQuery) 本番環境プロジェクト `merpay-prod` へのアクセスが検出されました。本当にアクセスして良いか確認してください。 5. シナリオレベル検出の実装例 シナリオレベル検出では、複数ノード間の関係性を解析します。代表的な実装例として、Slack User Validationシナリオを解説します。 5.1 検出対象のリスク Slack Triggerを使用したワークフローでは、以下のリスクがあります。 想定されるリスクシナリオ : 本来アクセス権を持たないメンバーがSlackボットにコマンドを送信し、アクセス権を持つメンバーのcredentialを使用して本番データベース等の機密リソースにアクセス 必要な対策 : Slackトリガーの直後にユーザーバリデーションを実装 外部アクセス(BigQuery、HTTP Request等)の前にバリデーションを完了 バリデーションに失敗した場合はワークフローを停止 5.2 検出の流れ Slack User Validationの検出は、以下の3ステップで実施されます。 DAG解析でバリデーションノードを探索 (5.3節): Slack Triggerから下流のノードを辿り、Code/Ifノードを探索 バリデーション実装を検証 (5.4節): 見つかったノードの内容を解析し、適切なユーザー検証が実装されているか確認 フロー整合性をチェック : バリデーション前に外部アクセス(BigQuery等)が存在しないか検証 5.3 DAGによるフロー解析 n8nのループ対応 : n8nではワークフロー内でループ(循環参照)を作成できます。しかし、フロー解析にはDAG(有向非巡環グラフ)が必要です。そのため、SCC(強連結成分分解)を用いてループを検出し、各SCCを1つのノードとして扱うことでDAGに変換します。 実装 ( workflow-graph.ts ): // Typescript // 1. 強連結成分(SCC)を検出 const sccs = stronglyConnectedComponents(this.graph); // 2. 各SCCを1つのノードとして扱い、Condensed DAGを構築 const condensedDAG = this.createCondensedDAG(sccs); // 3. トポロジカルソートで実行順序を決定 const executionOrder = topologicalSort(condensedDAG); この変換により、ループを含むワークフローでも確実にフロー解析が可能になります。 e.g. Slack Triggerからのパス解析 ( slack-user-validation/checker.ts ): // Typescript // 1. Slack Triggerからの全ての下流ノードを取得 const allDownstreamNodes = this.getAllDownstreamNodes(slackTriggerId); // 2. バリデーションノード候補を抽出(Code, If nodes) const codeNodes = allDownstreamNodes.filter( node => node.type === NODE_TYPES.CODE ); const ifNodes = allDownstreamNodes.filter( node => node.type === NODE_TYPES.IF ); // 3. Slack Triggerからバリデーションノードまでのパスを取得 const pathToNode = this.graph.getPathFromNodeToNode( slackTriggerId, codeNode.id ); // 4. パス上に外部アクセスノードがないか検証 const pathValid = this.isPathValid(pathToNode); 5.4 バリデーションノードの検証 バリデーションノード候補が見つかったら、ノードタイプに応じて内容を解析します。 Codeノードによるバリデーション ( jscode-validator.ts ) BabelのAST解析で以下の4要素を検出: User ID抽出 : $input.item.json.user または $json.user 許可ユーザーのホワイトリスト定義 : const users = { 'U123': 'user1', 'U456': 'user2' } 検証ロジック : !users.hasOwnProperty(userId) 等 エラーハンドリング : 検証失敗時の return または throw 4要素すべて揃っている場合は完全な実装と判定します。 Ifノードによるバリデーション ( if-node-validator.ts ) Ifノードの条件式で以下をチェック: メールアドレスパターン : 右辺値が会社ドメイン(例: @example.com )のパターンと一致 ユーザーコンテキスト抽出 : 左辺値にユーザー情報の抽出が含まれる 等価演算子 : equals 演算子による厳密なチェック If nodeで「 {{ $json.user }} が @example.com メールと一致するか」をチェックするパターンを検出可能です。 5.5 検出結果の例 Codeノード(javascript) 完全なユーザーバリデーション実装 // Javascript // Slack Triggerの直後のCode node const userId = $input.item.json.user; const users = { 'U0123ABCD': 'authorized_user1', 'U4567EFGH': 'authorized_user2' }; if (!users.hasOwnProperty(userId)) { return; // バリデーション失敗時に停止 } // 以降の処理 検出結果: ### ✅ OK (1) Slack TriggerのCodeノードで適切なユーザー検証が実装されています。承認済みユーザー数: 2 不完全なユーザーバリデーション実装 // Javascript // User ID抽出のみ実装(ホワイトリストなし) const userId = $input.item.json.user; // 検証ロジックなし 検出結果: ### 🚨 重大な問題 (Critical Issues) (1) - **[Slack Trigger]** (n8n-nodes-base.slackTrigger) Slack Triggerに対するユーザー検証が不完全です。以下の「不足している要素と修正方法」を参考にCodeノード "User Validation" を設定してください: - 不足している要素と修正方法: - 1. 認証リスト: オブジェクトとして定義(変数名は "users" である必要があります): `const users = { "userId1": "userName1", "userId2": "userName2" }` - 2. 検証ロジック: `if (!users.hasOwnProperty(userId))`, `if (!(userId in users))` などのチェックを追加 - 3. エラーハンドリング: 検証のif文内に `return` または `throw` ステートメントを追加 Ifノード Ifノードで以下のような条件を設定: 左辺値: {{ $json.user }} (Slackユーザー情報の抽出) 演算子: equals (等価演算子) 右辺値: @example.com (会社ドメインパターン) 検出結果: ### ✅ OK (1) Slack TriggerのIfノードで適切なユーザー検証が実装されています。 6. 検出の例および社内での活用事例 メルカリでは、このツールをGitHub Actionsに組み込み、ワークフロー追加時のPRで自動的にセキュリティチェックを実行しています。 6.1 問題のあるワークフローの検出例 以下のワークフローには複数のセキュリティ上の問題および懸念があります。 検出された問題 : Slack Trigger直後のユーザーバリデーションが未実装 BigQueryで本番環境(prod)プロジェクトへのアクセスを実行 Google Sheets作成後のアクセス権限スコープ設定が未実装 これらの問題は、PRコメントとして以下のように報告されます: Slack TriggerとGoogle Sheetsでは重大な問題として、BigQueryの本番環境アクセスについては警告として検出されます。 6.2 修正後のワークフローの検証例 上記の問題を修正したワークフローでは、以下の対策が実装されています。 実装された対策 : Ifノードによるユーザーバリデーション(メールドメインチェック) Google Driveノードによる適切なアクセス権限スコープ制御 そして、修正後のPRコメントでは、問題が解消されたことが確認できます。 ユーザーバリデーションが正しく実装されていることを検出し、スコープ制御の設定内容も詳細に表示されます。これにより、レビュアーはセキュリティ観点を即座に確認できるようになっています。 7. 成果と今後の展望 ツール導入によって得られた定量的な成果に加え、開発を通じて得られた技術的な知見、そして今後の展望について整理します。 7.1 定量的成果 レビュー工数の削減 : 手動レビュー時間: 30-60分/ワークフロー → 5-10分/ワークフロー 削減率: 約80-85% 検出精度 : 重大セキュリティリスク(🚨 Error)の検出率: 100% 偽陽性率: 約15%(⚠️ Warningレベル) 7.2 技術的知見 JSONからの情報抽出 : n8nのワークフローJSONには、ノード設定、接続情報、コード内容など、豊富な情報が含まれる この構造化データにより、他のワークフローツール(Zapier、Make等)よりも詳細な静的解析が可能 DAGによるフロー解析 : graphology をベースとしたグラフ構造で、ノード間の依存関係をDAGとしてモデル化 graphology-components でSCC(強連結成分分解)を実行し、ループをDAGに変換 graphology-dag のトポロジカルソートで実行順序を決定し、複雑なワークフローパターンを検出可能 特に「ユーザーバリデーション → 外部アクセス」の順序検証など、セキュリティ上重要な関係性を自動検証できる AST解析の有用性 : 正規表現では検出が難しい複雑なJavaScriptパターンを、ASTによる構造的な解析で実現 Babelのtraverseを使うことで、コードの意味を理解した検証が可能 n8n公式との型定義同期 : n8nは個別ノードの型定義を公式に提供していないため、このツールでは独自に型定義を作成 公式パッケージには TypeScript型定義 ではなく、 ランタイムスキーマ定義 ( versionDescription.properties )が含まれており、各パラメータの名前、型、必須/任意などの情報を持つ このスキーマ定義を活用し、Schema Validator ( bigquery/schema-validator.ts ) で自作型定義のキーと公式スキーマのプロパティ名を照合 不一致が検出された場合は警告を出力することで、公式の破壊的変更(パラメータ名変更、削除等)にも迅速に対応可能 7.3 今後の展望 検出ノードの拡充 : 現在対応しているノードタイプの拡大 LLMとの連携 : 検出結果に対する修正提案の自動生成 ワークフローの意図を理解した高度なセキュリティ分析 動的情報を活用した検出の強化 : Slack APIを用いたチャンネル属性の検証:チャンネルIDからPrivate/Publicを判定し、Public Channelへの投稿を警告 Credential情報から取得可能な権限スコープの検証:設定されたCredentialの実際の権限を確認し、過剰な権限付与を検出 8. まとめ 本記事では、n8nワークフローの静的セキュリティ解析ツールの開発について解説しました。 技術的ポイント : ワークフローJSONの豊富な情報活用 : ノード設定、接続情報、コード内容を完全に抽出可能 DAGによるフロー解析 : 複数ノード間の関係性を解析し、セキュリティ上重要なパターンを検出 AST解析 : JavaScriptコードを構造的に理解し、ユーザーバリデーションロジックの完全性を検証 ビジネスインパクト : 手動レビュー工数を80%削減 継続的な自動監視により、変更後のワークフローも安全性を保証 セキュリティポリシーの標準化と一貫性のある適用 ワークフロー自動化ツールの導入が進む中、本ツールのような静的解析アプローチは、他のプラットフォームにも応用可能です。業務効率化とセキュリティ確保の両立を目指す組織の参考になれば幸いです。 明日の記事は abcdefujiさんです。引き続きお楽しみください。 n8n.io logo source: https://n8n.io/brandguidelines/
動画
該当するコンテンツが見つかりませんでした











