Skip to main content

JavaScriptで安全にURLを検証する

著者

Mannan Tirmizi

feature argument injection

2023年5月9日

0 分で読めます

編集者注:2023年5月9日

2022年10月18日に公開されたこの記事を更新し、Snykが安全なJavaScript URL検証の確立にどのように役立つかをご紹介します。

ブラウザーの履歴の移動、アンカーリンクのターゲット、クエリパラメーターなど、目的に応じてさまざまな形式のURLを扱う必要があるとき、私たちはJavaScriptを使うことがよくあります。しかし、広く利用されているため、攻撃者に脆弱性を悪用されるリスクがあります。こうしたリスクを防ぐには、JavaScriptアプリケーションでURL検証を実装する必要があります。

URL検証では、すべてのURLが従うべき構造である、正しいURL構文に沿っているかを確認します。URL検証を行うことで、悪意のあるスクリプトのインジェクションやサーバーサイドリクエストフォージェリ(SSRF)など、URLを起点とする脆弱性からアプリケーションを守れます。リモートリソースを取得する際、ユーザーが指定したURLを検証するためのセキュアコーディングの慣行を守らなければ、攻撃者はSSRF攻撃を仕掛けることができます。SSRFは、フロントエンドとNode.jsのサーバーサイドアプリケーションの双方にとって、今も重大な脅威です。また、2021年のOWASP Top 10でも特集されたカテゴリです。

URL検証

URL検証は、悪用の可能性に対するセキュリティを強化し、コード実行時に発生するバグを防ぐために行います。では、いつURL検証を使い、どのような項目を確認すればよいのでしょうか。ページ、画像、GIF、動画などのリソースを特定して確認する必要のあるソフトウェアでは、すべてURL検証を実装すべきです。

一般的なURLは、プロトコル、ドメイン名、ホスト名、リソース名、オリジン、ポートなど、複数の要素で構成されています。これらの要素によって、ブラウザーは対象のリソースを取得する方法を判断します。URLは次のような方法で検証できます。

  • 正規表現リテラルとコンストラクターを使う

  • URLコンストラクター

  • isValidURLメソッド

  • 入力要素

  • アンカータグを使う方法

一般的なURL検証の仕組みでは、ユーザーからの入力を受け取り、解析してさまざまな構成要素を特定します。各URLの要素がインターネット標準に準拠しているかを確認できます。たとえば必要に応じて、安全なプロトコルが使われているかをチェックできます。

ホスト名の検証は、ホスト名を個別のラベルに分け、トップレベルドメイン名の仕様に準拠しているかを確認するところから始まります。一般的なホスト名は、ドットで区切られた2つ以上のラベルで構成されます。たとえば、www.snyk.comは「www」「snyk」「com」のラベルで構成されています。各ラベルに使用できるのは、大文字・小文字を問わず英数字またはハイフンのみです。さらに、検証の仕組みでホスト名を許可リストと照合することで、指定したURLだけを許可し、許可すべきURLを誤って拒否しないようにできます。

URLで使われるリソースへのパスは、通常、デフォルトですべて許可されます。一方、ポート番号は1〜65536の範囲でなければなりません。この範囲外の値が指定された場合は、エラーを発生させるべきです。また、数値形式のIPアドレスを確認し、IPv4アドレスかIPv6アドレスかを判定することもできます。

さらに、見落としがちですが、URLにユーザー名やパスワードが含まれているかどうかも確認できます。これにより、企業ポリシーへの準拠と認証情報の保護に役立ちます。

基本を確認したところで、JavaScriptでのURL検証方法を見ていきましょう。

JavaScriptでURLを検証する方法

JavaScriptでURLを検証する最も簡単な方法は、new URLコンストラクター関数を使うことです。シンプルなだけでなく、Node.jsランタイムとほとんどのブラウザーでサポートされています。

基本的な構文は次のとおりです。

new URL (url)
new URL (url , base)

相対URLを指定する場合、JavaScriptではbase要素が必要です。指定しない場合、デフォルトでundefinedになります。一方、絶対URLを指定したbase要素を渡した場合、JavaScriptはbase要素を無視します。

URLの検証には、次の関数を使用できます。

function checkUrl (string) {
    let givenURL ;
    try {
        givenURL = new URL (string);
    } catch (error) {
        console.log ("error is", error);
       return false; 
    }
    return true;
  }

この関数はURLが有効かどうかを確認し、有効な場合はtrue、そうでない場合はfalseを返します。www.urlcheck.comを渡すと、有効なURLスキームが含まれていないため、falseが返されます。このURLの正しい形式はhttps://urlcheck.comです。もう1つの例はmailto:John.Doe@example.comです。これは有効なURLですが、コロンを取り除くと、JavaScriptはURLとして認識しなくなります。3つ目の例はftp://です。ホスト名がないため、これは無効です。ドットを2つ追加すると(..)、そのドットがホスト名として扱われるため有効になり、ftp://..は有効なURLになります。

一見、変わったURLに思えても、実際には有効なものがある点に注意しましょう。開発者にとって予想外の形式でも、問題なく使える場合があります。たとえば、次のURLはどちらもTRUEを返します。

  • new URL("youtube://a.b.c.d");

  • new URL ("a://1.2.3.4@1.2.3.4");

これらの例は、慣例にとらわれず、URL検証の原則に基づいて判断するべきであることを示しています。

有効なURLに特定のURLスキームが含まれていることを確認するには、次の関数を使います。

  function checkHttpUrl(string) {
    let givenURL;
    try {
        givenURL = new URL(string);
    } catch (error) {
        console.log("error is",error)
      return false;  
    }
    return givenURL.protocol === "http:" || givenURL.protocol === "https:";
  }

この関数はURLを検証した後、HTTPまたはHTTPSのスキームが使われているかを確認します。ここでは、ftp://..はHTTPでもHTTPSでもないため無効ですが、http://..は有効です。URLコンストラクター関数のその他の使い方をいくつか紹介します。

  let m = 'https://snyk.io';
  let a = new URL("/", m);

上記の例ではbase要素を使用しています。値をログに出力すると、https://snyk.io/が得られます。

baseパラメーターを指定せずにURLオブジェクトを返す場合の構文は、次のとおりです。

  let b = new URL(m);

ホストにパス名を追加するには、次のように記述します。

  let d = new URL('/en-US/docs', b);

dに格納されるURLはhttps://snyk.io/en-US/docsです。

URLモジュールは、ブラウザーが使用するWHATWG URL標準に準拠したWHATWG URL APIも実装しています。

  let adr = new URL("https://snyk.io/en-US/docs");
  let host = adr.host;
  let path = adr.pathname;

上記の例では、adrというURLオブジェクトを作成しました。その後、コードでURLのホストとパス名を取得しています。それぞれsnyk.ioと/en-US/docsです。最後に、URLを許可リストまたは拒否リストと照合すれば、指定したURLだけを許可し、許可すべきURLを誤って拒否しないようにできます。

正規表現でURLを検証する方法 — ただし、おすすめしません

URLを検証する方法として、正規表現(regex)を使う方法もあります。正規表現とは、検索パターンを表す文字列です。URLが有効かどうかを確認するために使用できます。

正規表現を使ったURL検証のJavaScript構文は次のとおりです。

  function isValidURL(string) 
        {
            var res = 
            string.match(/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-
            ]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]
            \.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|w
            ww\.[a-zA-Z0-9]+\.[^\s]{2,})/gi);
        return (res !== null);
        };

いくつかのURLをテストしてみましょう。

  var tc1 = "http://helloworld.com"
  console.log(isValidURL(tc1));

正規表現で定義したURL構文は、URLがhttp://またはhttps://のスキーム、あるいはサブドメインで始まっているか、またドメイン名が含まれているかを確認します。コンソールの結果がtrueなのは、正規表現で定義されたURL構文に従っているためです。一方、次の文は許可されたスキームやサブドメインで始まっておらず、ドメイン名も含まれていないため、falseを返します。

  var tc4 = "helloWorld";
  console.log (isValidURL(tc4));

上記の正規表現は比較的シンプルですが、読み解くのは簡単ではありません。また、正規表現ではURL検証のルールを十分に処理できないため、エラーが発生しやすい方法です。正規表現でできるのは、有効なURLに一致するかを判定することに限られます。さらに、複雑な検証ロジックを含む場合や、長い入力文字列を処理する場合、検証に時間がかかります。

定義された正規表現の検証を完了するために、ブラウザーは入力文字列を何百万回もバックトラックする必要があります。このような過剰な再試行は「壊滅的なバックトラッキング」につながる可能性があります。複雑な正規表現によってブラウザーがフリーズしたり、CPUコアの処理能力を使い果たしたりする現象です。

Snykで脆弱なオープンソースパッケージを検出

メンテナーもURL検証を誤る可能性があり、その場合、Node.jsアプリケーションが危険にさらされます。npmパッケージのkeycloak-connectは、週5万回以上ダウンロードされているプロジェクトですが、2023年3月、安全でないURL検証の実装によりCVE-2023-2237のオープンリダイレクト脆弱性が発生することが公表されました。

脆弱なオープンソース依存関係を見つけて修正するために、ぜひSnykでプロジェクトを無料スキャンしてください。

Snykの脆弱性ページ。keycloak-connectのバージョン21.0.1未満にオープンリダイレクトがあり、深刻度は中程度の6.8と評価されています。

Snykが選ぶJavaScriptの脆弱性トップ10

このチートシートでは、2022年にSnykがJavaScriptアプリをスキャンして発見した、最も多く見られる重大度が「重大」および「高」のオープンソースの脆弱性を詳しく解説します。

実際に起きた脆弱性とその対策

Node.jsは、Node Package Manager(npm)というパッケージマネージャーを使用する、無料のオープンソースかつクロスプラットフォームのJavaScriptランタイム環境です。

2019年にも同様の事件が起こり、攻撃者が単独で、米国最大級の銀行Capital Oneのデータに不正アクセスしました。この情報漏えいは、今世紀でも特に重大な事件の1つとして知られています。The New York Timesによると、攻撃者は顧客1億人分の記録、社会保障番号14万件、Capital Oneの顧客に紐づく銀行口座情報8万件にアクセスしました。

攻撃者は、Amazon Web Services(AWS)でホストされているCapital Oneのサーバーにアクセスしました。検知が遅れたことから、攻撃者はAWSインフラストラクチャに精通しており、マネージドセキュリティサービスプロバイダー(MODSEC)のWebアプリケーションファイアウォール(WAF)に悪用可能な脆弱性があることを把握していたと考えられます。この知識をもとに攻撃者はSSRF攻撃を実行し、脆弱なWebサーバーを操作して新たなHTTPリクエストを送信し、AWSメタデータサービスへのアクセスに成功しました。

企業の要件に応じて、HTTPやHTTPSなど一部のプロトコルだけを許可することで、SSRF攻撃を防止できます。セキュリティをさらに高めるには、監視なしにURLパラメーターが渡されないよう、アプリケーションを設定する必要があります。一般的には許可リストと拒否リストを使って、仕組みへの入力をフィルタリングおよび制御し、SSRFの可能性を大幅に低減します。許可リストでは、事前に定義したオブジェクトやサーバーのみをアプリケーションで使用できます。一方、拒否リストでは、一般に利用可能なホスト名をアプリケーションが取得しないよう制限します。

JavaScriptを安全に使う

新しいOWASP Top 10にSSRFが追加されたことからもわかるように、JavaScriptアプリケーションのセキュリティにおいて、URL検証はますます重要になっています。幸い、サーバーサイドでURLを検証すれば、このような攻撃のリスクを軽減できます。また、URLの検証と処理に推奨される方法に従って、新しいURL関数を使うことも有効です。

新しいURL関数の機能とユースケースをいくつか確認し、正規表現を使ったURL検証の方法と、それが扱いにくくエラーを起こしやすい理由を見てきました。最後に、SSRFを引き起こしたJavaScriptの脆弱性について事例を紹介しました。

URLに伴うセキュリティリスクは、URLが有効かどうかよりも、危険なURLスキームに関係しています。そのため、サーバーサイドのアプリケーションで検証を行う必要があります。攻撃者はクライアントサイドの検証を回避できるため、それだけに頼るべきではありません。

アプリケーションセキュリティの管理やSnyk製品について詳しくは、ぜひSnykをご覧ください。

JavaScriptのセキュリティを守るSnyk

最初のコードから最後のnpm依存関係まで、SnykはIDE、CLI、GitのワークフローからJavaScriptアプリケーションを安全に保ちます。