URLを文字列結合で組み立てると色々問題が起きやすいので、それを避けるためにURL APIURLSearchParams 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を扱うときはURLURLSearchParamsを使う

先ほども書いていたように、URLに対して何か処理するときは、URLを構造的なオブジェクトとして扱わないと問題が起きやすいです。 現在のウェブブラウザ/Node.js/Deno/Bun/Cloudflare Workersなどは、URL APIとURLSearchParams APIというウェブ標準を実装しています。

そのため、URLから特定の部分を取得したい、特定のパラメータを変更したいなどURL処理を行う際には、URLURLSearchParamsを使います。

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で処理したことによる問題がいろいろなところで見つかっています。

せっかく実行環境にビルトインされててよくテストされたURLURLSearchParamsという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"

:memo: 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

おわりに

URLURLSearchParamsは便利です。 URL文字列をそのままの文字列結合、文字列置換、正規表現で処理してる場合、大部分はURLURLSearchParamsに置き換えられると思います。

特にパラメータはURLSearchParamsの方が、安全で分かりやすいコードになるはずなので、文字列処理でやる必要がほぼありません。 (同様のことをするものとしてNode.jsのquerystringqsがありますが、ほとんどの人はURLSearchParamsで十分だと思います。)

URLURLSearchParamsを使うときに使い方を参照できるものが欲しくて、URLチートシートを作りました。 もっといいレシピがあったら、是非Pull Requestを送ってください。

また、このリポジトリのサンプルコードはCIでテストしていて、そのテストにはpower-doctestというツールを使っています。 power-doctestは、MarkdownやAsciidocの中のコードブロックのコードを評価して、console.log(a); // => 結果がコメント通りになるかを自動テストできるツールです。

JavaScript Primer - 迷わないための入門書 #jsprimerでもこの仕組みを使って、ほとんどのサンプルコードは自動テストされています。

この記事のようなURLやパスを文字列としてそのまま扱うと問題が起きるという話は、jsprimerでも書いていました。