[JavaScript] URLを文字列結合で組み立てないために、url-cheatsheetを作った
URLを文字列結合で組み立てると色々問題が起きやすいので、それを避けるためにURL APIやURLSearchParams APIでURLを組み立てるパターンをまとめたチートシートを作りました。
URLにユーザーが入力した文字列を含めるときはencodeURIComponentでエスケープする
URLはプレーンな文字列ではなく構造化された文字列(文字の並びに意味がある文字列)として扱わないと、さまざまな問題を抱えやすいです。
たとえば、次のように文字列結合でURLを組み立てるとパストラバーサルの問題があります。
// DO NOT
const name = "<user input>"
const url = `https://example.com/user/${name}`;
name
に ../../admin
のような文字列が入ると、https://example.com/admin
というURLとして解釈されます。
URLの場合は、../
や/
などを文字列結合してもoriginは変更できないので、そこまで深刻にはなりにくいですが、サイト上にオープンリダイレクタがあると、別のドメインのURLとして解釈させられるようなケースもあります。
URLは、攻撃者が被害者に特定のURLを踏ませることが簡単(DMなどで誘導すればいいだけであるため、短縮URLなどもあり偽装はしやすい)であるため、いろいろな攻撃の開始点となりやすい部分です。
そのため、ユーザー入力を使ってURLを組み立てる場合は、そのユーザー入力はencodeURIComponent()でURLエスケープする必要があります。
// DO
const name = "<user input>"
const url = `https://example.com/user/${encodeURIComponent(name)}`;
同じように、URLのパラメータも単純な文字列結合で組み立ててしまうと、任意のパラメータを追加できたり、パラメータの上書きができたりと問題が起きやすいです。
また普通に利用していて、A&M
などのように特定の記号を正しく検索できないサイトにもなったりします。
// DO NOT
const query = "<user input>"
const url = `https://example.com?q=${query}`;
console.log(url); // => "https://example.com?q=<user input>"
このパラメータの問題も同じく、ユーザー入力をパラメータに使う場合はencodeURIComponent()する必要があります。
// DO
const query = "<user input>"
const url = `https://example.com?q=${encodeURIComponent(query)}`;
console.log(url); // => "https://example.com?q=%3Cuser%20input%3E"
パラメータに関しては、URLSearchParams()を使うと自動的に各パラメータをURLエスケープしてくれるので安全にパラメータを組み立てできます。
先ほどの例は、URLSearchParams() を使うと次のように書けます。
const q = "<use input>";
const params = new URLSearchParams({
q
});
console.log("https://example.com?" + params); // => "https://example.com/?q=%3Cuser%20input%3E
URLを扱うときはURLとURLSearchParamsを使う
先ほども書いていたように、URLに対して何か処理するときは、URLを構造的なオブジェクトとして扱わないと問題が起きやすいです。 現在のウェブブラウザ/Node.js/Deno/Bun/Cloudflare Workersなどは、URL APIとURLSearchParams APIというウェブ標準を実装しています。
そのため、URLから特定の部分を取得したい、特定のパラメータを変更したいなどURL処理を行う際には、URLとURLSearchParamsを使います。
new URL("https://example.com/path/to/page?q=query&page=1#main")
をしてみると、URL文字列からURLオブジェクトを作成して構造的なURLが見れます。
console.log(new URL("https://example.com/path/to/page?q=query&page=1#main"));
/* URL {
href: "https://example.com/path/to/page?q=query&page=1#main",
origin: "https://example.com",
protocol: "https:",
username: "",
password: "",
host: "example.com",
hostname: "example.com",
port: "",
pathname: "/path/to/page",
search: "?q=query&page=1"
hash: "#main"
searchParams: URLSearchParams { q → "query", page → "1" }
} */
URLを正しくパースするのは至難の技で、URLパーサを脆弱性なしに実装できる人は限られています。 毎年、独自のURLパーサーの脆弱性、URLを単純にreplace/splitで処理したことによる問題がいろいろなところで見つかっています。
- Incorrect returned href via an ‘@’ sign but no user info and hostname · CVE-2022-0639 · GitHub Advisory Database
- What Bypassing Razer’s DOM-based XSS Patch Can Teach Us - EdOverflow
- A New Era of SSRF - Exploiting URL Parser in Trending Programming Languages! - us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf
せっかく実行環境にビルトインされててよくテストされたURLとURLSearchParamsというAPIがあるので、ぜひこれを使いましょう。
というのが、url-cheatsheetを書いた理由です。
いろいろなレシピを書いているので、詳細はリポジトリを参照してください。 また、こういうパターンがあるといいとかあったらPull Requestを送ってください。
一部を抜粋すると次のような逆引きリファレンス的なチートシートになってます。
Base URL + Path
new URL(pathname, base)
が利用できます。
const base = "https://example.com";
const pathname = "/path/to/page";
const result = new URL(pathname, base);
console.log(result.toString()); // => "https://example.com/path/to/page"
URL
は自動的にパスをエスケープしてくれるわけではないので、ユーザー入力を含む場合は必ずencodeURIComponent
でエスケープする必要があります。
const base = "https://example.com/";
const name = "<user input>"
const result = new URL(`/user/${encodeURIComponent(name)}`, base);
console.log(result.toString()); // => "https://example.com/user/%3Cuser%20input%3E"
Get parameter from URL
Use URL
and URLSearchParams#get
解説: URL
でパースすると自動的にsearchParams
というプロパティにパラメーターをパースしたものが含まれます。
const inputURL = "https://example.com/?q=query&page=1";
const url = new URL(inputURL);
const q = url.searchParams.get("q");
console.log(q); // => "query"
Get multiple parameters as array from URL
Use URL
and URLSearchParams#getAll
解説: 同じパラメータ名を複数指定した場合は、get
ではなくgetAll
でまとめて取得できます。
const inputURL = "https://example.com/?q=query&lang=en_US&lang=ja_JP";
const url = new URL(inputURL);
const langs = url.searchParams.getAll("lang");
console.log(langs); // ["en_US", "ja_JP"]
Add parameters to URL
Use URLSearchParams
解説: URLSearchParamsのオブジェクトはtoString()
すると、自動的にパラメータの文字列となります。
const q = "query";
const page = 1;
const base = "https://example.com";
const url = new URL(base);
const params = new URLSearchParams({
q,
page
});
console.log(url + "?" + params); // => "https://example.com/?q=query&page=1"
または、url.search
プロパティに代入すると、URLオブジェクトのパラメータを上書きできるので、?
を手書きしなくてもよくなります。
const q = "query";
const page = 1;
const base = "https://example.com";
const url = new URL(base);
url.search = new URLSearchParams({
q,
page
});
console.log(url.toString()); // => "https://example.com/?q=query&page=1"
URLSearchParams
escape each parameter automtically.
const q = "<user input>";
const page = 1;
const base = "https://example.com";
const url = new URL(base);
url.search = new URLSearchParams({
q,
page
});
console.log(url.toString()); // => "https://example.com/?q=%3Cuser+input%3E&page=1"
Update parameter of URL
Use URL
’s searchParams
property.
const inputURL = "https://example.com/?q=query&page=1";
const url = new URL(inputURL);
url.searchParams.set("q", "update");
console.log(url.toString()); // => "https://example.com/?q=update&page=1"
Remove parameter from URL
Use URL
and URLSearchParams
const inputURL = "https://example.com/?q=query&page=1";
const url = new URL(inputURL);
url.searchParams.delete("q");
console.log(url.toString()); // => "https://example.com/?page=1"
Filter parameters
Allow only a
and d
parameters.
解説: searchParams
はIteratorなので、Array.from(searchParams)
すると [key, value]
の配列へと変換できます。あとはフィルターした結果から新しいsearchParamsを作成しています。
const base = "https://example.com/?a=1&b=2&c=3&d=4";
const url = new URL(base);
const allowedParameterNames = ["a", "d"];
url.search = new URLSearchParams(Array.from(url.searchParams).filter(([key, value]) => {
return allowedParameterNames.includes(key);
}));
console.log(url.toString()); // => "https://example.com/?a=1&d=4"
Check URL is valid
URL
throw an error when parsing invalid url string.
解説: new URL
はパースできないURL文字列が渡された場合は、例外を投げます。例外を投げるかでURLとしてvalidなのかが判定できます。
const isValidURL = (urlString) => {
try {
new URL(urlString); // if `urlString` is invalid, throw an erorr
return true;
} catch {
return false;
}
};
console.log(isValidURL("https://example.com")); // => true
console.log(isValidURL("https/example.com")); // => false
おわりに
URLとURLSearchParamsは便利です。
URL文字列をそのままの文字列結合、文字列置換、正規表現で処理してる場合、大部分はURL
とURLSearchParams
に置き換えられると思います。
特にパラメータはURLSearchParamsの方が、安全で分かりやすいコードになるはずなので、文字列処理でやる必要がほぼありません。
(同様のことをするものとしてNode.jsのquerystring
やqsがありますが、ほとんどの人はURLSearchParamsで十分だと思います。)
URLとURLSearchParamsを使うときに使い方を参照できるものが欲しくて、URLチートシートを作りました。 もっといいレシピがあったら、是非Pull Requestを送ってください。
また、このリポジトリのサンプルコードはCIでテストしていて、そのテストにはpower-doctestというツールを使っています。
power-doctestは、MarkdownやAsciidocの中のコードブロックのコードを評価して、console.log(a); // => 結果
がコメント通りになるかを自動テストできるツールです。
- MarkdownやAsciidoc中に書いたJavaScriptのサンプルコードをdoctestするツールを作った | Web Scratch
- azu/power-doctest: JavaScript Doctest for JavaScript, Markdown and Asciidoc.
JavaScript Primer - 迷わないための入門書 #jsprimerでもこの仕組みを使って、ほとんどのサンプルコードは自動テストされています。
この記事のようなURLやパスを文字列としてそのまま扱うと問題が起きるという話は、jsprimerでも書いていました。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。