- 安全なコードを書きたくても、レビューや脆弱性診断まで気付けないことは多いです。
- 今回は、javascript/typescript向けの静的解析ツール「fallow」のsecurity機能で危険な箇所の「候補」を出し、自分で確認して直す流れを試します。
環境
- Node.js v24.20.0
- npm 11.9.0
- fallow 3.30.0
手順
fallow導入
- プロジェクトを作成し、fallowを入れます。
mkdir fallow-sample && cd fallow-sample
mkdir src
npm init -y
npm install --save-dev fallow
npx fallow init
-
npx fallow initで.fallowrc.jsonが作られます。
検査カテゴリの設定
-
.fallowrc.jsonを次の内容に書き換えます。 -
security.categories.includeで、検査するカテゴリを選びます。
.fallowrc.json
{
"$schema": "./node_modules/fallow/schema.json",
"security": {
"categories": {
"include": [
"dangerous-html",
"code-injection",
"command-injection",
"sql-injection",
"ssrf",
"path-traversal",
"open-redirect",
"weak-crypto",
"hardcoded-secret"
]
}
}
}
- カテゴリの一覧:公式ドキュメント
- カテゴリの詳細:
npx fallow explain <カテゴリ名>(例:npx fallow explain dangerous-html) - 今回のサンプルで使うカテゴリ:
-
command-injection:コマンドインジェクション -
sql-injection:SQLインジェクション -
dangerous-html:XSS -
hardcoded-secret:秘密鍵の直書き
-
include に書いていないカテゴリは検出されません。hardcoded-secret も、書いたときだけ動きます。
脆弱なサンプルコード用意
- 動作確認用に、脆弱なコードを4つ用意します。
コマンドインジェクション
-
src/command.tsを作成して、以下の内容を記述します。
import { exec } from "node:child_process";
export function pingHost(name: string) {
exec("ping -c 1 " + name, (err, stdout) => {
console.log(stdout);
});
}
name に ; rm -rf / を渡されると、任意のコマンドが実行されます。
SQLインジェクション
-
src/sql.tsを作成して、以下の内容を記述します。
type Db = { query: (sql: string) => unknown };
export function findUser(db: Db, id: string) {
return db.query(`SELECT * FROM users WHERE id = ${id}`);
}
id に 1 OR 1=1 を渡されると、全ユーザーが取得されます。
XSS
-
src/xss.tsを作成して、以下の内容を記述します。
export function showComment(text: string) {
const el = document.querySelector("#comment");
if (el) el.innerHTML = text;
}
text に <img src=x onerror=...> を渡されると、閲覧者のブラウザでスクリプトが動きます。
秘密鍵直書き
-
src/secret.tsを作成して、以下の内容を記述します。
export const stripeSecretKey = "sk_live_xxxxxxxxxxxxxxxxxx";
ソースに書くと、gitの履歴に残り続けます。値はダミーです。
検出を確認
- 以下を実行します。
npx fallow security
- 私の環境では、4件の候補が出ました。
[L] low OS command injection sink (CWE-78) src/command.ts:4
[L] low Hardcoded secret candidate (CWE-798) src/secret.ts:1
[L] low SQL injection sink (CWE-89) src/sql.ts:4
[L] low Dangerous HTML sink (CWE-79) src/xss.ts:3
- 各候補には、根拠(
evidence)と次にやること(Next)などが付きます。
修正して再確認
コマンドインジェクション
-
src/command.tsを以下の内容に書き換えます。
import { execFile } from "node:child_process";
export function pingHost(name: string) {
if (!/^[a-zA-Z0-9.-]+$/.test(name)) throw new Error("bad host");
execFile("ping", ["-c", "1", name], (_err, stdout) => console.log(stdout));
}
シェルを通さず、引数を配列で渡します。入力の形式も検証します。
SQLインジェクション
-
src/sql.tsを以下の内容に書き換えます。
type Db = { query: (sql: string, params: unknown[]) => unknown };
export function findUser(db: Db, id: string) {
return db.query("SELECT * FROM users WHERE id = ?", [id]);
}
値を文字列に埋め込まず、プレースホルダで渡します。
XSS
-
src/xss.tsを以下の内容に書き換えます。
export function showComment(text: string) {
const el = document.querySelector("#comment");
if (el) el.textContent = text;
}
HTMLとして解釈させず、テキストとして入れます。
秘密鍵直書き
-
src/secret.tsを以下の内容に書き換えます。
export const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
環境変数から読みます。
再実行
- 再度
npx fallow securityを実行して検知されないことを確認します。
Result: 0 security items to check.
まとめ
-
fallow securityは、脆弱性を断定せず、根拠つきの候補を出すツールです。判定は自分で行う役割分担が明確で、続けやすいと感じました。 - 本記事では触れませんでしたが、gitのpre-commitやCI、AIにも組み込めるので、危険コードをpush前や公開前に防ぐ仕組みも構築できると感じました。
- ただし、カテゴリ設定に依存し、見逃しや死角もあります。レビューや他の診断と併用するのが良さそうです。