Gixy-Nginxの設定を安全にするYandexのオープンソース

Nginxは間違いなく最もクールなWebサーバーの1つです。 ただし、適度にシンプルで、非常に拡張性があり、生産的であるため、自分への敬意が必要です。 ただし、これは、サービスのセキュリティと可用性が依存するほとんどすべてのソフトウェアに適用されます。 Nginxが好きです。 Yandexでは、単純なリバースプロキシから本格的なアプリケーションまで、さまざまな構成の膨大な数のインストールによって表されます。 この多様性により、私たちは共有したい[安全でない]構成の経験を積んでいます。







しかし、まず最初に。 NginxはWebアプリケーションの完全なキューブであるため、Nginxの安全な構成に長い間苦しめられてきました。つまり、その構成は、アプリケーション自体のコードよりも私たちの側での制御を必要とします。 昨年、このプロセスには深刻な自動化が必要であることが明らかになりました。 そこで、 Gixy社内プロジェクトが始まりました。要件は次のとおりです。



-シンプルであること。

-しかし、拡張可能。

-テストプロセスに便利に統合できる機能。

-インクルージョンを解決できると便利です。

-および変数を操作します。

-正規表現を忘れないでください。



率直に言って、最後まで(GolangとPythonの間で)言語の選択をためらっていました。 その結果、Pythonがより一般的になることを期待して選ばれました。つまり、開発が少し簡単になるということです。



問題について



この紹介を終了し、一般的な問題の例に移ります:)将来の混乱を避けるため、すべての例では、現在のメインラインバージョンのNginx-1.13.0を使用しました。



サーバー側リクエスト偽造



Server Side Request Forgeryは、Webアプリケーションに代わって(この場合はNginxに代わって)さまざまな種類のクエリを実行できる脆弱性です。 これは、攻撃者がプロキシされたサーバーのアドレスを制御できる場合に発生します。たとえば、 XSendfileの構成が正しくない場合などです



私自身の経験から、この脆弱性はいくつかのエラーに関連していることが多いと言えます。



- 内部ディレクティブの欠如。 その意味は、特定の場所が内部クエリにのみ使用できることを示すことです。

-安全でない内部リダイレクト。



最初のケースですべてが明確な場合、内部リダイレクトでは事態はそれほど単純ではありません。 私はあなたの多くが同様の構成を見た/書いたと思う:



location ~* ^/internal-proxy/(?<proxy_proto>https?)/(?<proxy_host>.*?)/(?<proxy_path>.*)$ { internal; proxy_pass $proxy_proto://$proxy_host/$proxy_path ; proxy_set_header Host $proxy_host; }
      
      





残念ながら、この構成では、ドキュメントによると、少なくともすべてのrewriteおよびtry_filesディレクティブを確認する必要があります。



内部リクエストは次のとおりです。

-error_page、index、random_index、 try_filesディレクティブによってリダイレクトされたリクエスト。

-上流サーバーの応答ヘッダーの「X-Accel-Redirect」フィールドを使用してリダイレクトされた要求。

-ngx_http_ssi_moduleモジュールの「include virtual」コマンドとngx_http_addition_moduleモジュールディレクティブによって生成されたサブクエリ。

-rewriteディレクティブによって変更されたリクエスト。


不注意な書き換えを行うと、内部の場所でリクエストを行うことができます。 これは非常に簡単に確認できます。



-設定:



 location ~* ^/internal-proxy/(?<proxy_proto>https?)/(?<proxy_host>.*?)/(?<proxy_path>.*)$ { internal; return 200 "proto: $proxy_proto\nhost: $proxy_host\npath: $proxy_path"; } rewrite ^/(?!_api)(.*)/\.files/(.*)$ /$1/.download?file=$2 last;
      
      





-操作:



 GET /internal-proxy/http/evil.com/.files/some HTTP/1.0 Host: localhost HTTP/1.1 200 OK Content-Length: 42 Content-Type: application/octet-stream Date: Fri, 28 Apr 2017 13:55:51 GMT Server: nginx/1.13.0 proto: http host: evil.com path: .download
      
      





この状況では、通常、いくつかのプラクティスをお勧めします。



-プロキシには内部ロケーションのみを使用します。

-可能であれば、ユーザーデータの転送を禁止します。

-プロキシされるサーバーのアドレスを保護します。



•プロキシされるホストの数が限られている場合(たとえば、S3がある場合)、ハードコードし、マップまたは他の便利な方法を使用して選択することをお勧めします。

•何らかの理由でプロキシに使用できるすべてのホストをリストできない場合は、署名する価値があります。



リファラーまたはオリジンを検証するための不正な正規表現



問題があります。 正規表現を使用して解決することにしました。

「今、2つの問題があります。」


多くの場合、RefererまたはOrigin要求ヘッダーの検証は、正規表現を使用して行われます。 これは、条件付きでX-Frame-Optionsヘッダーを設定する(ClickJacking保護)か、クロスオリジンリソースシェアリング( CORS )を実装するために必要になることがよくあります。 そして、「Referer」の検証が少し簡単で、条件によっては、 ngx_http_referer_moduleモジュールを優先して正規表現を拒否できますが、「Origin」ではすべてがそれほど単純ではありません。



問題の主な2つのクラスを区別します。



-正規表現の準備のエラー。

-信頼できないサードパーティドメインの解決。



問題のある構成は次のとおりです。



 if ($http_origin ~* ((^https://www\.yandex\.ru)|(^https://ya\.ru)/)) { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Credentials' 'true'; }
      
      





実際、正規表現を大幅に簡略化しましたが、この例でも、最初に問題を見るのはそれほど簡単ではありません。 人々は正規表現を読むよりも書く方が簡単です。



幸いなことに、この問題はこのマシンでは一般的ではないため、Gixyはこの正規表現がwww.yandex.ru.evil.comを有効な発信元としてチェックし、それについて通知することを独自に判断できます。



 $ gixy --origins-domains yandex.ru,ya.ru /etc/nginx/nginx.conf ==================== Results =================== Problem: [origins] Validation regex for "origin" or "referrer" matches untrusted domain. Description: Improve the regular expression to match only trusted referrers. Additional info: https://github.com/yandex/gixy/blob/master/docs/ru/plugins/origins.md Reason: Regex matches "https://www.yandex.ru.evil.com" as a valid origin. Pseudo config: include /etc/nginx/sites/default.conf; server { server_name _; if ($http_origin ~* ((^https://www\.yandex\.ru)|(^https://ya\.ru)/)) { } }
      
      





または、ya.ruが十分に信頼されていない場合、 ya.ruおよびwww.yandex.ru.evil.comの起源を報告します。



 $ gixy --origins-domains yandex.ru /etc/nginx/nginx.conf ==================== Results =================== Problem: [origins] Validation regex for "origin" or "referrer" matches untrusted domain. Description: Improve the regular expression to match only trusted referrers. Additional info: https://github.com/yandex/gixy/blob/master/docs/ru/plugins/origins.md Reason: Regex matches "https://www.yandex.ru.evil.com", "https://ya.ru/" as a valid origin. Pseudo config: include /etc/nginx/sites/default.conf; server { server_name _; if ($http_origin ~* ((^https://www\.yandex\.ru)|(^https://ya\.ru)/)) { } }
      
      





HTTP分割



HTTP分割は、Nginx(HTTP要求分割)またはアプリケーションクライアント(HTTP応答分割)の背後にあるアプリケーションを攻撃するために使用されます。 この脆弱性は、攻撃者がNginxによって生成されたリクエストまたはレスポンスに改行文字を埋め込むことができる場合に発生します。



信頼できるアドバイスはありませんが(注意することを除いて)、常にいくつかのことに注意する必要があります。



-リクエストの生成を担当するディレクティブで使用される変数(CRLFを含めることができます)。たとえば、rewrite、return、add_header、proxy_set_header、proxy_pass;

-使用される変数$ uriおよび$ document_uriです。使用されている場合は、urldecoded値が含まれていることが保証されているため、どのディレクティブで使用されますか。

-排他的範囲(?P [^。] +)を持つグループから取得した変数に特に注意してください。



除外範囲の例:



-設定:



 server { listen 80 default; location ~ /v1/((?<action>[^.]*)\.json)?$ { add_header X-Action $action; return 200 "OK"; } }
      
      





-操作:



 GET /v1/see%20below%0d%0ax-crlf-header:injected.json HTTP/1.0 Host: localhost HTTP/1.1 200 OK Content-Length: 2 Content-Type: application/octet-stream Date: Fri, 28 Apr 2017 13:57:28 GMT Server: nginx/1.13.0 X-Action: see below x-crlf-header: injected OK
      
      





ご覧のとおり、応答ヘッダーx-crlf-header:を追加できました。 これは、いくつかの状況の組み合わせが原因で発生しました。



-add_headerは、作成者が結果を認識していると仮定して、渡された値をエンコード/検証しません。

-場所の処理前にパスの値が正規化されます。

-$アクション変数は、排他的範囲を持つ正規表現グループから選択されました:[^。] *;

-したがって、$アクション変数の値は、以下を参照するようになりました\ r \ nx-crlf-header:HTTP応答に挿入されて落ちました。



幸いなことに、Gixyはこのタスクで非常に成功しています。



-彼は「危険な」変数を知っています-より正確には、ほとんどの組み込み変数の許容される文字セットを知っています。 したがって、$ request_uriと$ uriの違いは明らかです。

-正規表現グループから変数を抽出できます。

-任意の文字(この場合は\ n)が正規表現(または単一のグループ)によってスマッキングできるかどうかを判断できます。



もう1つの興味深い例は、try_filesによる書き換えです。



-設定:



 server { listen 80 default; location / { try_files $uri $uri/ /index.php?q=$uri; } location ~ \.php { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Host $host; proxy_pass http://127.0.0.1:9000; } }
      
      





-操作(127.0.0.1:9000でデバッグエコーサーバーをリッスン):



 GET /request%20HTTP/1.0%0aInjection: HTTP/1.0 Host: localhost HTTP/1.1 200 Ok Content-Length: 244 Content-Type: text/plain Date: Fri, 28 Apr 2017 13:59:18 GMT Server: nginx/1.13.0 GET /index.php?q=/request HTTP/1.0\n Injection: HTTP/1.0\r\n X-Real-IP: 127.0.0.1\r\n X-Forwarded-For: 127.0.0.1\r\n Host: localhost\r\n Connection: close\r\n User-Agent: HTTPie/0.9.8\r\n Accept-Encoding: gzip, deflate\r\n Accept: */*\r\n \r\n
      
      





どうする



-より安全な変数、たとえば$ uriの代わりに$ request_uriを使用してください。

-/ some /(?[^ /] +の代わりに、/ some /(?[^ / \ S] +)などの排他的範囲での改行を禁止します。

-おそらく、$ uri検証を追加することをお勧めします(実行していることがわかっている場合のみ)。



add_headerディレクティブで「上位」の応答ヘッダーをオーバーライドする



これはよく知られたNginxの機能であり、私たちの多くがつまずき、つまずき続けます。 一番下の行は非常に単純です-ヘッダーが同じレベル(サーバーセクションなど)にインストールされ、下位レベルが設定されている場合(場所など)、最初のヘッダーは適用されません。



最も簡単な例は次のとおりです。



 server { listen 80 default; server_name _; add_header X-Content-Type-Options nosniff; location / { add_header X-Frame-Options DENY; } }
      
      





この場合、場所/を処理するときに応答ヘッダーX-Content-Type-Optionsは設定されません。



ギクシーはこれについてうまく伝えます:



 $ gixy /etc/nginx/nginx.conf ==================== Results =================== Problem: [add_header_redefinition] Nested "add_header" drops parent headers. Description: "add_header" replaces ALL parent headers. See documentation: http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header Additional info: https://github.com/yandex/gixy/blob/master/docs/ru/plugins/addheaderredefinition.md Reason: Parent headers "x-content-type-options" was dropped in current level Pseudo config: include /etc/nginx/sites/default.conf; server { server_name _; add_header X-Content-Type-Options nosniff; location / { add_header X-Frame-Options DENY; } }
      
      





この問題を解決するいくつかの方法を知っています。



-重要なヘッダーを複製します。

-サーバーセクションなど、同じレベルでヘッダーを設定します。

-ngx_headers_moreモジュールの使用を検討してください。



それぞれに長所と短所があります。 どちらを選ぶかはあなた次第です。



Gixyについて



Nginxの構成には細心の注意が必要であることを納得していただければ幸いです。 また、Nginx構成の静的分析が機能すると考えています(これにより、 Nginx Amplifyのエクスペリエンスも確認されます)。 残念ながら、Nginxの背後にあるアプリケーションのすべての境界線のケースまたは特定の機能を自動的に検出できるとは限りません。 そのため、たとえば、X-Forwarded- *リクエストヘッダーをオーバーライドするためのチェックを標準セットに含めませんでした。それらへの反応はアプリケーションによって異なり、場合によっては(たとえば、複数のプロキシを使用して)まったく触れてはいけないからです。 しかし、自宅では、アプリケーションのより深い理解に基づいて、必要なチェックを行うことができます。 はい、現在、Gyxyは私たちが知っている問題の全範囲を特定する方法を知りませんが、勉強中であり、あなたの助けを借りて、おそらくより良く、より完全にそれを始めます。



使用シナリオについて話す場合、私たちはいくつかの典型的なケースを特定しました。



-nginxがインストールされているテスト環境で起動します。

-単一のブロックをチェックするためのWebアプリケーション。 これは、構成の不審な部分に遭遇したときに役立ちます。

-CIまたはシンクライアントと統合するためのHTTP API。



最も興味深いオプションは、シンクライアントにHTTP APIを使用することです。 実際、この場合、必要なチェックを一元管理し、更新することができます。 幸いなことに、nginxの最新バージョンには、設定とそのダンプをテストするための-Tスイッチがあり、Gixyはこの形式を解析できます。



それがどれほど便利かを自分で判断してください
$ nginx -T | http -v https://gixy/api/check Content-Type:'application/nginx'

POST /api/check HTTP/1.1

Accept: application/json, */*

Accept-Encoding: gzip, deflate

Connection: keep-alive

Content-Length: 959

Content-Type: application/nginx

Host: gixy

User-Agent: HTTPie/0.9.8



# configuration file /etc/nginx/nginx.conf:

user http;

worker_processes 1;



#daemon on;

events {

worker_connections 1024;

}



http {

include mime.types;

default_type application/octet-stream;

sendfile on;

keepalive_timeout 65;

gzip on;

access_log /var/log/nginx/access.log combined;

error_log /var/log/nginx/error.log debug;



include sites/*.conf;

}



# configuration file /etc/nginx/mime.types:

types {

text/html html htm shtml;

text/css css;

text/xml xml;

image/gif gif;

image/jpeg jpeg jpg;

application/javascript js;

application/atom+xml atom;

application/rss+xml rss;

}



# configuration file /etc/nginx/sites/default.conf:

server {

listen 80;

return 301 https://some$uri;

}



HTTP/1.1 200 OK

Connection: keep-alive

Content-Encoding: gzip

Content-Type: application/json

Date: Tue, 24 Apr 2017 19:45:57 GMT

Keep-Alive: timeout=120

Server: nginx

Transfer-Encoding: chunked



{

"result": [

{

"auditor": "http_splitting",

"config": "\ninclude /etc/nginx/sites/default.conf;\n\n\tserver {\n\t\treturn 301 https://some$uri;\n\t}",

"description": " (\"\\n\") nginx. : rewrite, return proxy_pass.",

"help_url": "https://wiki/product-security/gixy/httpsplitting/",

"reason": "At least variable \"$uri\" can contain \"\\n\"",

"recommendation": " , (eg \"$request_uri\" \"$uri\").",

"severity": "HIGH",

"summary": " HTTP Splitting"

}

],

"status": "ok",

"warnings": []

}








最後に、これがGixyの最初のパブリックアルファバージョンであるため、後方互換性を維持せずにAPIを変更できるという事実を強調したいと思います。 この点で、独自のプラグインを実装する必要がある場合は、Issueを作成するか、プルリクエストを送信することをお勧めします。その後、一緒に何かを考えます。



私たちの経験があなたにとって興味深く、有益であり、おそらくあなたが再びあなたの構成を再考することさえしたことを願っています;)



All Articles