API8 分で読める

REST API のクエリパラメータを正しくエンコードする方法

REST API のクエリパラメータは、&、=、スペース、Unicode といった特殊文字を安全に含めるために、RFC 3986 に従ってパーセントエンコードする必要があります。JavaScript では encodeURIComponent()、Python では urllib.parse.quote()、Java では URLEncoder.encode() を使いましょう。

クエリパラメータにエンコードが必要な理由

REST API のクエリ文字列は、キーと値のペアを & で区切り、キーと値を = で区切るという構造化された形式を使います。もしパラメータの値にこれらの区切り文字や、#+、スペースといった予約文字が含まれていると、クエリ文字列の構造が壊れ、サーバーは誤ったデータを受け取ってしまいます。

たとえば次のような例を考えてみましょう。API で「salt & pepper」を検索したいとします。エンコードしない場合、URL /search?q=salt & pepper はサーバーに対して、q=salt pepper(値なし)という 2 つのパラメータがあると伝えてしまいます。正しくエンコードすれば、/search?q=salt%20%26%20pepper は「salt & pepper」という値を持つ単一のパラメータ q を正しく送信します。

正確性の問題だけでなく、適切なエンコードはセキュリティ上の脆弱性も防ぎます。URL 内にエンコードされていないユーザー入力が含まれると、インジェクション攻撃やキャッシュポイズニングなどのエクスプロイトにつながる可能性があります。URL に含める前に、パラメータの値は必ずエンコードしましょう。

各言語でのエンコード

主要なプログラミング言語はいずれも、URL エンコード用の組み込み関数を備えています。ここでは、代表的な言語でクエリパラメータを正しくエンコードする方法を紹介します。

// JavaScript
// 個々のパラメータ値には encodeURIComponent() を使う
const query = 'price >= 100 & category = books';
const url = 'https://api.example.com/search?q=' + encodeURIComponent(query);
// "https://api.example.com/search?q=price%20%3E%3D%20100%20%26%20category%20%3D%20books"

// 複数のパラメータには URLSearchParams を使ってもよい
const params = new URLSearchParams({
  q: 'price >= 100 & category = books',
  page: '1',
  sort: 'price'
});
const url2 = 'https://api.example.com/search?' + params.toString();
# Python
from urllib.parse import urlencode, quote

# 複数のパラメータには urlencode を使う
params = {
    'q': 'price >= 100 & category = books',
    'page': 1,
    'sort': 'price'
}
query_string = urlencode(params)
url = f'https://api.example.com/search?{query_string}'

# 単一の値には quote を使う
value = quote('price >= 100 & category = books', safe='')
url = f'https://api.example.com/search?q={value}'
// Java
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

String query = "price >= 100 & category = books";
String encoded = URLEncoder.encode(query, StandardCharsets.UTF_8);
// "price+%3E%3D+100+%26+category+%3D+books"
// 注意: URLEncoder はスペースを + でエンコードする(フォームエンコード)
        
// RFC 3986 のエンコードにするには + を %20 に置き換える
String rfc3986 = encoded.replace("+", "%20");
String url = "https://api.example.com/search?q=" + rfc3986;
// C#
using System.Net;
using System.Web;

string query = "price >= 100 & category = books";

// Uri.EscapeDataString は RFC 3986 に準拠する
string encoded = Uri.EscapeDataString(query);
// "price%20%3E%3D%20100%20%26%20category%20%3D%20books"

// HttpUtility.UrlEncode はフォームエンコードを使う(スペースは +)
string formEncoded = HttpUtility.UrlEncode(query);
// "price+%3e%3d+100+%26+category+%3d+books"
// Go
package main

import (
    "fmt"
    "net/url"
)

func main() {
    // url.QueryEscape はクエリ文字列向けにエンコードする(スペースは +)
    encoded := url.QueryEscape("price >= 100 & category = books")
    // "price+%3E%3D+100+%26+category+%3D+books"

    // url.PathEscape はパスセグメント向けにエンコードする(スペースは %20)
    pathEncoded := url.PathEscape("my file name.pdf")
    // "my%20file%20name.pdf"

    // url.Values を使ってクエリ文字列を組み立てる
    params := url.Values{}
    params.Set("q", "price >= 100 & category = books")
    params.Set("page", "1")
    queryString := params.Encode()
    fmt.Println(queryString)
}

配列やネストしたオブジェクトのエンコード

REST API では、配列やネストしたオブジェクトをクエリパラメータとして受け取る必要がしばしば生じます。これらの複雑な型のエンコードには単一の標準規格が存在しないため、API ごとに異なる慣習が使われています。

// キーの繰り返し(最も広くサポートされている)
// GET /api/items?tag=javascript&tag=python&tag=go
const params = new URLSearchParams();
['javascript', 'python', 'go'].forEach(tag => params.append('tag', tag));

// ブラケット記法(PHP、Rails、qs を使った Express などで一般的)
// GET /api/items?tags[]=javascript&tags[]=python&tags[]=go
// 手動でエンコードする必要がある:
const tags = ['javascript', 'python', 'go'];
const qs = tags.map(t => 'tags[]=' + encodeURIComponent(t)).join('&');

// カンマ区切り(シンプルだが制約がある)
// GET /api/items?tags=javascript,python,go
const tagList = encodeURIComponent('javascript,python,go');

// ネストしたオブジェクト(ブラケット記法)
// GET /api/search?filter[status]=active&filter[min_price]=10
// Python の場合:
params = {
    'filter[status]': 'active',
    'filter[min_price]': '10'
}

期待される形式については、必ず利用する API のドキュメントを確認してください。新しい API を設計する場合は、キーを繰り返すアプローチが、プログラミング言語や HTTP ライブラリをまたいで最も移植性が高い方法です。

サーバー側でのデコードのベストプラクティス

サーバー側で受信リクエストを処理する際、ほとんどの Web フレームワークはクエリパラメータを自動的にデコードしてくれます。とはいえ、堅牢なサーバー側処理のために念頭に置くべき重要なポイントがいくつかあります。

  • ほとんどのフレームワーク(Express、Django、Spring、ASP.NET)はクエリパラメータを自動的にデコードします。手動で再度デコードしないでください。二重デコードになってしまいます。
  • デコードした値は、使用する前に、期待される型や範囲に照らして検証してください。
  • URL の最大長の上限を Web サーバーのレベルで設定してください(一般的には 2048 文字または 8192 文字)。
  • コンテンツタイプによっては + がスペースを意味する場合とリテラルのプラス記号を意味する場合があるため、その両方に対応してください。
  • デバッグのためには、デコード後の値はログ上で誤解を招くことがあるので、元の(エンコードされた)URL を記録してください。
// Express.js — パラメータは自動的にデコードされる
app.get('/search', (req, res) => {
  const query = req.query.q; // すでにデコード済み
  // 次のようにはしないこと: decodeURIComponent(req.query.q)

  // 入力を検証する
  if (typeof query !== 'string' || query.length > 200) {
    return res.status(400).json({ error: 'Invalid query parameter' });
  }

  // デコード済みの値を安全に使う
  const results = search(query);
  res.json(results);
});

セキュリティ上の考慮点

適切なクエリパラメータのエンコードは、API セキュリティの重要な要素です。ここでは、守るべき最も重要なセキュリティ上の実践を紹介します。

  • クライアント側のエンコードを決して信用しないこと。 クライアントが正しくエンコードしていたとしても、デコード後のパラメータは必ずサーバー側で検証・サニタイズしてください。
  • 二重エンコード攻撃に注意すること。 攻撃者は、一度しかデコードしない入力フィルタを回避するために、悪意ある文字を二重にエンコードすることがあります。セキュリティフィルタが完全にデコードされた値を見るようにしてください。
  • パストラバーサルを防ぐこと。 パスパラメータ内の %2e%2e%2f(../)のようなエンコードされたシーケンスは、意図したディレクトリの外のファイルにアクセスするために悪用され得ます。ファイル操作の前にパスをデコードして検証してください。
  • パラメータ化されたクエリを使うこと。 URL デコードした後であっても、ユーザー入力を SQL や NoSQL のクエリに直接連結してはいけません。パラメータ化されたクエリや ORM を使ってください。
  • URL の長さを制限すること。 極端に長いクエリ文字列はサービス拒否(DoS)を引き起こす可能性があります。妥当な長さを超える URL を拒否するよう Web サーバーを設定してください。
  • 出力もエンコードすること。 クエリパラメータの値を HTML レスポンスに反映して返す際は、クロスサイトスクリプティング(XSS)攻撃を防ぐために HTML エンティティエンコードを適用してください。

関連記事

無料ツールを試す