Azure DevOps ArtifactsのREST APIで「A potentially dangerous Request.Path value was detected from the client (:)」が出る原因と回避策【Universalパッケージ対応】

Azure DevOps Artifacts の Universal パッケージを REST API で取得しようとしたところ、パッケージ名にコロン (:) を含めてパスに差し込むと「A potentially dangerous Request.Path value was detected from the client (:)」という 400 エラーで落ちる――この現象は、IIS/ASP.NET のリクエスト検証が「:」を危険文字として判定することが直接原因です。本記事では、なぜ起きるのかを噛み砕いて解説し、設計で回避する方法から最小範囲での設定緩和、実運用に耐える具体的な API 呼び出し例まで、コピー&ペーストで使える形で整理します。

目次

エラーの実態と再現条件

以下のように、パッケージ名にコロンを含め、それを URL のパス(Path) に埋め込んだ場合に発生します。URL エンコードで %3A にしても防げません。

GET https://pkgs.dev.azure.com/{organization}/{project}/_apis/packaging/feeds/{feed}/packages/MavenProject.Automationtesting:Automationtesting/versions/1.0.0/content?api-version=7.0
--> HTTP/1.1 400 Bad Request
A potentially dangerous Request.Path value was detected from the client (:).

コロンを %3A にしても同様です(ASP.NET 側でデコード後に検証されるため)。

なぜ「:」が危険文字扱いされるのか

  • ASP.NET(System.Web)には Request Path 検証 があり、既定で < > * % & : \ ? などをパスに含むことを拒否します。
  • 検証は デコード後の値 に対して行われるため、:%3A にしても効果がありません。
  • 同じ「URL」でも パスクエリ文字列 では扱いが異なり、パスは厳格に、クエリは比較的寛容に取り扱われます。

ありがちな誤解:URL エンコードで回避できる?

できません。ASP.NET はリクエストパイプラインの初期段階でパスをデコードし、その結果に対して危険文字チェックを行います。したがって %3A: として判定され、同じ 400 を返します。

最短で分かる対処方針(結論)

対応内容メリット / 注意点
① 危険文字を使わない(推奨)パッケージ名や API の パス: < > * % & \ ? を置かない。名称を変更する / 代替識別子に置換する / クエリ文字列・ヘッダーで渡す。最も安全・シンプル。システム全体のセキュリティ方針に合致。
② ID or クエリで参照(実務的)パッケージ で検索(クエリ)→得た packageId で以降の API を実行。
コロンはパスに置かず、クエリ文字列にのみ入れる。
URL 設計を変えずに通せる。Azure DevOps の REST 仕様にも沿う。
③ CLI / 公式ツールを使用az artifacts universal download や ArtifactTool を使い、名前やバージョンは引数で指定(内部で正しく処理される)。手戻りが少ない。CI/CD パイプラインに組み込みやすい。
④ ASP.NET 側の検証緩和System.Web アプリでは web.confighttpRuntimerequestPathInvalidCharacters を調整。必要最小限のスコープに限定する。強力だが攻撃面を広げる。内部ネットワーク限定など運用ガードが必須。
⑤ ASP.NET Core / IIS 設定ASP.NET Core は System.Web の検証がない。IIS リバースプロキシ配下なら requestFiltering を点検。基本は ①②③を優先。プラットフォーム差異に注意。アプリでのサニタイズを維持。

推奨アプローチ 1:危険文字をパスに使わない設計

最終的に運用コストが最も低く、脆弱性監査にも通しやすいのが「危険文字をパスに置かない」方針です。既に「MavenProject.Automationtesting:Automationtesting」のような命名を使っている場合は、次のいずれかに揃えると後が楽になります。

命名ポリシー備考
セグメント置換(:--MavenProject.Automationtesting--Automationtesting人間可読。双方向マッピングが容易。
スラグ化(英数と-のみ)mavenproject-automationtesting-automationtesting検索性・一貫性に優れる。
ID 化(GUID/ULID)01HR6M4QJ8J7X3A9B4P2K7VY5N衝突回避・照合が高速。表示名は別フィールドで管理。

既存名称を維持したままでも、パスに載せない(後述のクエリ/ID 参照に切替)だけでこの問題は消えます。

推奨アプローチ 2:Azure DevOps REST を「クエリ+ID」で使う

Azure DevOps の Packaging API は、パッケージ名で検索(クエリ)して ID を取得し、その ID で詳細やバイナリへ進むという運用ができます。肝は「コロンをパスに含めない」こと。名前はクエリ文字列で渡せます。

手順(概略)

  1. 検索: パッケージ名に : を含んだまま クエリ パラメータで検索(例:packageNameQuery)。
  2. 選定: レスポンスから対象パッケージの id(GUID)と目的の version を得る。
  3. 取得: 以降の API は /packages/{packageId}/versions/{versionId} など ID ベースのパスで呼ぶ。パスにコロンが出ない。

curl 例(PAT 認証)

# 1) パッケージ名で検索(名前はクエリに渡す:パスに入れない)
ORG={organization}
PROJ={project}
FEED={feed}
PAT={your_pat}

curl -u :$PAT 
"[https://pkgs.dev.azure.com/$ORG/$PROJ/_apis/packaging/feeds/$FEED/packages?packageNameQuery=$(python](https://pkgs.dev.azure.com/$ORG/$PROJ/_apis/packaging/feeds/$FEED/packages?packageNameQuery=$%28python) -c 'import urllib.parse;print(urllib.parse.quote("MavenProject.Automationtesting:Automationtesting"))')&protocolType=upack&api-version=7.0"

# 2) 応答から packageId を取り出す(例:id = "f1e2d3...-guid")

# 3) ID でバージョン一覧・コンテンツ取得(ID ベースのパスなので : は現れない)

PACKAGE_ID={packageId}
VERSION={version}

curl -L -u :$PAT 
"[https://pkgs.dev.azure.com/$ORG/$PROJ/_apis/packaging/feeds/$FEED/packages/$PACKAGE_ID/versions?api-version=7.0](https://pkgs.dev.azure.com/$ORG/$PROJ/_apis/packaging/feeds/$FEED/packages/$PACKAGE_ID/versions?api-version=7.0)"

# Universal パッケージのコンテンツ取得(例)

VERSION_ID={versionId}
curl -L -u :$PAT 
"[https://pkgs.dev.azure.com/$ORG/$PROJ/_apis/packaging/feeds/$FEED/packages/$PACKAGE_ID/versions/$VERSION_ID/content?api-version=7.0](https://pkgs.dev.azure.com/$ORG/$PROJ/_apis/packaging/feeds/$FEED/packages/$PACKAGE_ID/versions/$VERSION_ID/content?api-version=7.0)" -o package.upack 

PowerShell 例(そのまま実行可能)

$org   = "{organization}"
$proj  = "{project}"
$feed  = "{feed}"
$pat   = "{your_pat}"

$base64Pat = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
$headers = @{ Authorization = "Basic $base64Pat" }

# 1) 名前で検索(コロンはクエリにだけ含める)

$name = "MavenProject.Automationtesting:Automationtesting"
$q = [System.Uri]::EscapeDataString($name)
$searchUrl = "[https://pkgs.dev.azure.com/$org/$proj/_apis/packaging/feeds/$feed/packages?packageNameQuery=$q&protocolType=upack&api-version=7.0](https://pkgs.dev.azure.com/$org/$proj/_apis/packaging/feeds/$feed/packages?packageNameQuery=$q&protocolType=upack&api-version=7.0)"

$search = Invoke-RestMethod -Headers $headers -Uri $searchUrl -Method GET
$pkg = $search.value | Select-Object -First 1
$packageId = $pkg.id

# 2) バージョン情報

$versionsUrl = "[https://pkgs.dev.azure.com/$org/$proj/_apis/packaging/feeds/$feed/packages/$packageId/versions?api-version=7.0](https://pkgs.dev.azure.com/$org/$proj/_apis/packaging/feeds/$feed/packages/$packageId/versions?api-version=7.0)"
$versions = Invoke-RestMethod -Headers $headers -Uri $versionsUrl -Method GET
$version = $versions.value | Select-Object -First 1
$versionId = $version.id

# 3) コンテンツ取得

$contentUrl = "[https://pkgs.dev.azure.com/$org/$proj/_apis/packaging/feeds/$feed/packages/$packageId/versions/$versionId/content?api-version=7.0](https://pkgs.dev.azure.com/$org/$proj/_apis/packaging/feeds/$feed/packages/$packageId/versions/$versionId/content?api-version=7.0)"
Invoke-WebRequest -Headers $headers -Uri $contentUrl -OutFile "package.upack" 

C#(HttpClient)例:クエリ→ID→取得の流れ

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class Program
{
static async Task Main()
{
var org  = "{organization}";
var proj = "{project}";
var feed = "{feed}";
var pat  = "{your_pat}";


    using var http = new HttpClient();
    var token = Convert.ToBase64String(Encoding.ASCII.GetBytes(":"+pat));
    http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token);

    // 1) 名前検索(クエリ)
    var name = "MavenProject.Automationtesting:Automationtesting";
    var url = $"https://pkgs.dev.azure.com/{org}/{proj}/_apis/packaging/feeds/{feed}/packages" +
              $"?packageNameQuery={Uri.EscapeDataString(name)}&protocolType=upack&api-version=7.0";

    using var res1 = await http.GetAsync(url);
    res1.EnsureSuccessStatusCode();
    using var doc1 = await JsonDocument.ParseAsync(await res1.Content.ReadAsStreamAsync());
    var packageId = doc1.RootElement.GetProperty("value")[0].GetProperty("id").GetString();

    // 2) バージョン取得(ID ベース)
    var url2 = $"https://pkgs.dev.azure.com/{org}/{proj}/_apis/packaging/feeds/{feed}/packages/{packageId}/versions?api-version=7.0";
    using var res2 = await http.GetAsync(url2);
    res2.EnsureSuccessStatusCode();
    using var doc2 = await JsonDocument.ParseAsync(await res2.Content.ReadAsStreamAsync());
    var versionId = doc2.RootElement.GetProperty("value")[0].GetProperty("id").GetString();

    // 3) コンテンツダウンロード(ID ベース)
    var url3 = $"https://pkgs.dev.azure.com/{org}/{proj}/_apis/packaging/feeds/{feed}/packages/{packageId}/versions/{versionId}/content?api-version=7.0";
    var bytes = await http.GetByteArrayAsync(url3);
    await System.IO.File.WriteAllBytesAsync("package.upack", bytes);
}


} 

推奨アプローチ 3:CLI / ArtifactTool を使う

Azure CLI には Universal パッケージ専用のコマンドがあります。名前にコロンを含む場合も、引数として渡すだけで済み、内部で適切に処理されます。

az artifacts universal download \
  --organization "https://dev.azure.com/{organization}" \
  --project "{project}" \
  --feed "{feed}" \
  --name "MavenProject.Automationtesting:Automationtesting" \
  --version "1.0.0" \
  --path .

CI/CD(Azure Pipelines, GitHub Actions, 他)でも、CLI/ArtifactTool をタスクとして呼ぶだけなので、アプリ側・サーバー側設定を触らず短時間で安定運用に入れます。

やむを得ずサーバー側で緩和する場合(System.Web / IIS)

レガシーな System.Web(.NET Framework)アプリで、パスに「:」を含む URL をどうしても受けたい場合は、web.configrequestPathInvalidCharacters を調整します。必ず限定スコープ(特定の仮想ディレクトリ/コントローラ配下)で適用してください。

最小スコープでの緩和例(location 要素で限定)

&lt;configuration&gt;
  &lt;location path="api/packages"&gt;
    &lt;system.web&gt;
      &lt;!-- 既定の危険文字からコロンのみ除外(他は既定のまま) --&gt;
      &lt;httpRuntime requestPathInvalidCharacters="&lt;,&gt;,* ,%,&amp;,\,?" requestValidationMode="2.0" /&gt;
      &lt;pages validateRequest="false" /&gt;
    &lt;/system.web&gt;
  &lt;/location&gt;
&lt;/configuration&gt;

重要:検証を緩めるほど XSS やオープンリダイレクト等の攻撃面が広がります。少なくとも次を併用してください。

  • IP 制限・認証の前段適用(リバースプロキシ/WAF)。
  • 入力値の サーバー側 サニタイズとエンコード(HTML・JavaScript・URL・SQL の各コンテキスト)。
  • ルーティングで許容パターン(正規表現)を厳密化。
  • Fuzzing を含む脆弱性テストの自動化(CI に統合)。

ASP.NET MVC / Web API での局所無効化

特定のアクションだけで一時的に受けたい場合は次のような 属性 で範囲を絞る選択肢もあります。

[ValidateInput(false)]
public ActionResult Download(string name, string version) { ... }

ただし、モデルバインド経由の取り回しやビュー出力時のエンコードを厳密に行わないと危険です。基本は「パスに入れない」を守るのが正解です。

ASP.NET Core の場合の要点

  • ASP.NET Core は System.Web の Request Validation を持ちません。Kestrel 単体ではこのエラーは発生しません。
  • IIS リバースプロキシ配下であれば、IIS の requestFilteringsystem.webServer/security/requestFiltering)の影響を受けます。基本設定はコロンで詰まりにくいですが、独自の禁止シーケンス指定や WAF ルールでブロックされることはあります。
  • いずれにせよ、パスにコロンを置かないID ベースのパスクエリで名前を渡すの三原則は Core でも変わりません。

プロキシ/ゲートウェイ/WAF 配下での落とし穴

  • リバースプロキシや API ゲートウェイが 正規化(Normalization) を行うと、重複エンコードやデコード順序の違いで 400 になることがあります。二重エンコードを禁止してください。
  • ロギングは エッジ(プロキシ)とバックエンド(IIS/アプリ) の双方で有効化し、同一の相関 ID を通して追跡できるようにします。
  • WAF のシグネチャで : を含むパスパターンがブロックされていないか確認します(多くの場合、クエリで渡せば回避できます)。

実運用に落とすための設計チェックリスト

  • URL 設計: パス・クエリ・ヘッダーの責務分担を明文化(パス=リソースの ID のみ)。
  • 命名規約: 許容文字一覧・置換規則・衝突回避ルール(大文字小文字、トリム、NFC/NFD 正規化)。
  • 互換マッピング: 旧名称(コロンあり)→新名称(置換後)を辞書管理。移行期間は 301/308 リダイレクト。
  • 監査ログ: 入力(オリジナル)と正規化後を両方記録。GDPR/個人情報方針に従う。
  • テスト: 危険文字を含むパラメータの e2e テスト、フェイルオープンが起きないことの検証。

よくある質問(FAQ)

Q1. URL を「ダブルエンコード」すれば通る?
通りません。サーバーのどこかで最終的にデコードされ、結局コロンとして判定されます。ダブルエンコードは別の 400(無効な要求)や 404.11(IIS の二重エスケープ拒否)を招くため非推奨です。

Q2. パスではなくクエリに入れれば本当に安全?
少なくとも本件の例外は回避できます。ただしクエリに入れても、アプリ側の入力検証・エンコードは必須です。「パスに置かない」ことと「入力を無条件で信頼する」ことは別の話です。

Q3. ルータ(ASP.NET MVC/Core)のルート制約で許すことはできる?
ルータだけでは ASP.NET / IIS の前段検証を超えられません。サーバー全体の設定を緩めるか、パスに置かない設計へ改修してください。

Q4. 既に多数のコロン付きパッケージがある。最小工数は?
まずは REST を「クエリ→ID」に切替え、ダウンロードは CLI/ArtifactTool に寄せます。並行して名称の正規化(置換 or ID 化)を始めるのが運用と安全性のバランスが良いです。

Q5. Universal 以外(npm、NuGet 等)でも同じ?
パスに危険文字を置かない」「ID で参照」はどのプロトコルでも有効です。プロトコル固有の API でも、検索→ID→取得の流れは大差ありません。

実装テンプレート:安全なエンドポイント設計サンプル

アプリ側でコロン入り名称を受けつつ、パスには置かないエンドポイント設計例です。

# 例:GET /api/packages/content?name={複合名}&version={ver}
# name は "MavenProject.Automationtesting:Automationtesting" のようにコロン可
# サーバー側で name を検証・正規化し、内部では ID に解決して取得

GET /api/packages/content?name=MavenProject.Automationtesting%3AAutomationtesting&version=1.0.0 

この場合のルール:

  • パスは /api/packages/content 固定(危険文字なし)。
  • クエリの name は入力検証(長さ・文字種・許容表現)をかけ、内部で packageId に解決。
  • 監査ログにはオリジナルの name と解決後 packageId を併記。

セキュリティ観点のまとめ

選択肢攻撃面(増減)推奨度
パスに危険文字を置かない増えない最高
クエリで渡す / ID 参照適切な入力検証が前提
サーバー設定で緩和増える(防御層が薄くなる)低(限定スコープのみ)

最終まとめ

  • エラーの正体は、ASP.NET の Request.Path 検証が「:」を危険文字として拒否しているため。
  • 最優先は「パスに危険文字を置かない」設計。名称変更が難しければ クエリや ID 参照に切替える。
  • Universal パッケージの取得は「名前で検索 → ID で取得」が最も現実的。CLI/ArtifactTool も有効。
  • 設定緩和は最小範囲・短期間にとどめ、アクセス制限・サニタイズ・脆弱性テストを必ず併用。

付録:すぐ使えるコピペ集

web.config(System.Web・最小緩和の雛形)

&lt;configuration&gt;
  &lt;location path="api/packages"&gt;
    &lt;system.web&gt;
      &lt;httpRuntime requestPathInvalidCharacters="&lt;,&gt;,* ,%,&amp;,\,?" requestValidationMode="2.0" /&gt;
      &lt;pages validateRequest="false" /&gt;
    &lt;/system.web&gt;
  &lt;/location&gt;
&lt;/configuration&gt;

Azure CLI(Universal ダウンロード)

az artifacts universal download \
  --organization "https://dev.azure.com/{organization}" \
  --project "{project}" \
  --feed "{feed}" \
  --name "MavenProject.Automationtesting:Automationtesting" \
  --version "1.0.0" \
  --path .

REST 呼び出しの柱(疑似コード)

1) GET /_apis/packaging/feeds/{feed}/packages?packageNameQuery={name}&amp;protocolType=upack
2) GET /_apis/packaging/feeds/{feed}/packages/{packageId}/versions
3) GET /_apis/packaging/feeds/{feed}/packages/{packageId}/versions/{versionId}/content

PowerShell(最短 10 行版)

$h=@{Authorization="Basic "+[Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$env:PAT"))}
$base="https://pkgs.dev.azure.com/$env:ORG/$env:PROJ/_apis/packaging/feeds/$env:FEED"
$pkg=Invoke-RestMethod -Headers $h -Uri "$base/packages?packageNameQuery=$([uri]::EscapeDataString($env:NAME))&amp;protocolType=upack&amp;api-version=7.0"
$pid=$pkg.value[0].id
$ver=Invoke-RestMethod -Headers $h -Uri "$base/packages/$pid/versions?api-version=7.0"
$vid=$ver.value[0].id
Invoke-WebRequest -Headers $h -Uri "$base/packages/$pid/versions/$vid/content?api-version=7.0" -OutFile "package.upack"

この問題は「エンコード・トリックで乗り切る」類の話ではなく、URL 設計と取得フローを正すことで根本的に解消できます。まずは パスから危険文字を追放し、クエリ+ID 参照へ切り替える――それが最短で堅牢な道です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次