はじめに
SeleniumでBasic認証が行われているページにアクセスする場合は、以下のようにするとアクセスできます(Chromeの場合)。
driver.get("http://username:password@example.com")
この時、ユーザ名が例えば、mail@example.com
のようなメールアドレスの場合は、@
を%40
に置き換えて、以下のようにします。
driver.get("http://mail%40example.com:password@example.com")
@
や#
等、URLの中で特別な意味を持つ文字は、パーセントエンコーディングしてあげる必要があります。
参考:Percent-encoding (パーセントエンコーディング) | MDN Web Docs
setExtraHTTPHeadersを使ってBasic認証に対応させる(※注意事項あり)
ここからが本題で、以下の投稿で使ったChrome DevTools ProtocolにあるNetwork.setExtraHTTPHeadersメソッドを使って、Basic認証を突破することに挑戦してみます。
Network.setExtraHTTPHeaders
は、HTTPヘッダを付与することのできるメソッドで以下のように使います。
custom_headers = {
"X-Custom-Header1": "value1",
"X-Custom-Header2": "value2",
}
driver.execute_cdp_cmd("Network.enable", {})
driver.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": custom_headers})
最初に結論と注意事項としたことを書くと、以下のようになります。
-
setExtraHTTPHeaders
を使って、Basic認証は突破できる - ただし、認証対象ページ内にある別ドメインのリソース読み込み等で、ヘッダから認証情報が漏れる可能性がある
Basic認証突破の作戦
Basic認証の仕組みについては、下記の記事に詳しく記載されていました。詳しくはリンク先記事をご参照ください。
Basic認証では「Authorization」ヘッダに認証情報を付けて、ブラウザからサーバに送信します。このヘッダの付与をsetExtraHTTPHeaders
メソッドを使って実現する、というのが今回のBasic認証対応の試みです。
Basic認証を行うサイトの環境づくり
まずは、検証用にBasic認証がかけられたページを準備します。
Docker公式のApacheイメージをベースに作成します。
FROM httpd:2.4
RUN echo 'AuthUserFile /usr/local/apache2/htdocs/.htpasswd\n\
AuthGroupFile /dev/null\n\
AuthName "Basic Auth"\n\
AuthType Basic\n\
Require valid-user' > /usr/local/apache2/htdocs/.htaccess
RUN htpasswd -b -c /usr/local/apache2/htdocs/.htpasswd user@example.com secret
RUN sed -i.bak -e "s/AllowOverride None/AllowOverride All/g" /usr/local/apache2/conf/httpd.conf
以下のようなHTMLコンテンツも用意しておきます。
jQueryは実際には使っていませんが、後ほどの確認用にCDNからの読み込みコードを入れておきます。
<html>
<head>
<title>Basic Auth Page</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<body>
<h1>Secret Page</h1>
</body>
上記からDockerのイメージを作成し、index.html
をドキュメントルートにマウントして起動します。
$ docker build -t basic-auth-apache .
$ docker run -dit --name apache-app -p 8080:80 -v "$PWD/index.html":/usr/local/apache2/htdocs/index.html basic-auth-apache
ブラウザで、http://localhost:8080/index.html にアクセスすると、認証ダイアログが表示され、設定したID/パスワードを入力するとページが表示されました。
Seleniumコード
setExtraHTTPHeaders
メソッドを利用して、Basic認証に対応させたページにアクセスするSeleniumのコードは下記です。
import base64
import time
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
# 与えられた認証情報をもとに、Authorizationヘッダを作成するメソッド
def get_auth_header(user, password):
b64 = "Basic " + base64.b64encode('{}:{}'.format(user, password).encode('utf-8')).decode('utf-8')
return {"Authorization": b64}
# Webdriver ManagerでChromeDriverを取得
driver = webdriver.Chrome(executable_path=ChromeDriverManager().install())
# Authorizationヘッダを付与
driver.execute_cdp_cmd("Network.enable", {})
driver.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": get_auth_header("user@example.com", "secret")})
# Basic認証が必要なページにアクセス
driver.get('http://localhost:8080/index.html')
time.sleep(5)
driver.close()
driver.quit()
実行すると、Basic認証で保護されたページが表示されました。
気になっていること
Chrome DevTools ProtocolのNetwork.setExtraHTTPHeadersメソッドの説明には、以下のような記述があります。
Specifies whether to always send extra HTTP headers with the requests from this page.
この「always」という部分の記述が気になり、
常にAuthorizationヘッダが送られているのではないか?
(=認証情報が外部のサイトに送信されている状態でないか。)
という懸念を抱きました。
そこで、リクエスト内容を取得して確認することにしました。
リクエスト内容の取得
リクエスト内容の取得については、下記の記事を参考にさせていただきました。
Basic認証の検証用ページにアクセスし、その後、別サイトにアクセスするという流れの時のリクエスト内容(URLとAuthorizationヘッダの値)をファイルに記録するコードです。
import base64
import time
import json
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
# 与えられた認証情報をもとに、Authorizationヘッダを作成するメソッド
def get_auth_header(user, password):
b64 = "Basic " + base64.b64encode('{}:{}'.format(user, password).encode('utf-8')).decode('utf-8')
return {"Authorization": b64}
# ネットワークに関するログを取得するための設定
capabilities = DesiredCapabilities.CHROME
capabilities['goog:loggingPrefs'] = { 'performance': 'ALL' }
# Webdriver ManagerでChromeDriverを取得
driver = webdriver.Chrome(
executable_path=ChromeDriverManager().install(),
desired_capabilities=capabilities
)
# Authorizationヘッダを付与
driver.execute_cdp_cmd("Network.enable", {})
driver.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": get_auth_header("user@example.com", "secret")})
# Basic認証が必要なページにアクセス
driver.get('http://localhost:8080/index.html')
time.sleep(5)
# 別のサイトにアクセス
driver.get('https://qiita.com/')
# ネットワーク情報からリクエストURL・Authorizationヘッダの値を抽出し、ファイルに書き出す
with open('network.log', 'w') as f:
for entry_json in driver.get_log('performance'):
entry = json.loads(entry_json['message'])
if entry['message']['method'] != 'Network.requestWillBeSent' :
continue
print(entry['message']['params']['request']['url'],
# Authorizationヘッダがない場合は、「No Authorization header」と出力する
entry['message']['params']['request']['headers'].get('Authorization', "No Authorization header"),
file=f)
driver.close()
driver.quit()
確認結果
実行すると次のような結果が得られました。
localhost
のコンテンツ以外にも、HTMLから読み込んでいるCDNのjQuery取得のリクエスト、別サイト(Qiita)へのアクセス時にも認証情報が付与されてしまっていることがわかりました。
http://localhost:8080/index.html Basic dXNlckBleGFtcGxlLmNvbTpzZWNyZXQ=
https://code.jquery.com/jquery-3.6.0.min.js Basic dXNlckBleGFtcGxlLmNvbTpzZWNyZXQ=
http://localhost:8080/favicon.ico Basic dXNlckBleGFtcGxlLmNvbTpzZWNyZXQ=
https://qiita.com/ Basic dXNlckBleGFtcGxlLmNvbTpzZWNyZXQ=
・・・(省略)・・・
上記から、setExtraHTTPHeaders
メソッドでAuthorizationヘッダを設定する際の注意事項として、
- Authorizationヘッダの認証情報が外部のサイトに送信される
という点に注意が必要です。
一応、setExtraHTTPHeaders
メソッドのheaders
パラメータに{}
を指定するとカスタムヘッダの付与はされなくなるようです。
# Authorizationヘッダを付与
driver.execute_cdp_cmd("Network.enable", {})
driver.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": get_auth_header("user@example.com", "secret")})
# Basic認証が必要なページにアクセス
driver.get('http://localhost:8080/index.html')
time.sleep(5)
# カスタムヘッダの付与をクリア
driver.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": {}})
# 別のサイトにアクセス
driver.get('https://qiita.com/')
time.sleep(5)
上記のコードで実行すると、QiitaへのAuthorizationヘッダの送信は止まりました。
同じページ内で、別ドメインからのjQueryのJSファイル読み込みについては、試した範囲では回避はできませんでした。
http://localhost:8080/index.html Basic dXNlckBleGFtcGxlLmNvbTpzZWNyZXQ=
https://code.jquery.com/jquery-3.6.0.min.js Basic dXNlckBleGFtcGxlLmNvbTpzZWNyZXQ=
http://localhost:8080/favicon.ico Basic dXNlckBleGFtcGxlLmNvbTpzZWNyZXQ=
https://qiita.com/ No Authorization header
・・・(省略)・・・
おわりに
今回は、setExtraHTTPHeaders
メソッドを試してみました。Basic認証は一応突破できたものの、ページ内で別ドメインからのリソース読み込みを行っている場合は、認証情報を送信してしまう恐れがあるため、適用場面は限られてしまうのかなと思います。
setExtraHTTPHeaders
メソッド自体は、他のヘッダ付与にもちろん利用できるので、他の活用があるかもしれません。