EC-CUBE 4系の本番サーバーで、.envのDATABASE_URLから接続情報を切り出してmysqlコマンドで直接データベースに接続しようとしたところ、パスワードは合っているはずなのにAccess deniedになりました。原因は、.envのパスワードがURLエンコードされた状態で書かれていたことです。この記事では実際のエラーと、bashだけでデコードして接続するスクリプトを載せます。
起きたこと
調査のため、.envのDATABASE_URLをsedで分解してmysqlクライアントに渡したところ、次のエラーになりました。
ERROR 1045 (28000): Access denied for user 'appuser'@'10.0.1.23' (using password: YES)
不思議なのは、サイト自体は正常に動いていることです。アプリケーションは同じ.envで接続できているので、.envに書かれている資格情報そのものが間違っているわけではありません。
原因はパスワードのURLエンコード
DATABASE_URLはURI形式です。
DATABASE_URL=mysql://appuser:p%40ss%23w0rd%21@db.example.internal:3306/example_db
パスワードに @ : / # % などの記号が含まれる場合、URIとして壊れないようパーセントエンコードして書く必要があります(Symfonyの仕様)。上の例では、実際のパスワード p@ss#w0rd! が p%40ss%23w0rd%21 と書かれています。
アプリケーション側(Doctrine)はURIをパースするときに自動でデコードするので、サイトは問題なく動きます。しかしシェルで文字列を切り出してそのままmysqlコマンドに渡すと、エンコードされたままのパスワードを送ってしまうため認証に失敗します。「サイトは動いているのにCLIだけ弾かれる」という症状になるのはこのためです。
bashでデコードして接続する
実際に使ったスクリプトです。EC-CUBEのルートディレクトリで実行します。
URL=$(grep "^DATABASE_URL=" .env | head -1 | cut -d= -f2-)
stripped=${URL#mysql://}
creds=${stripped%%@*}
DBUSER=${creds%%:*}
DBPASS=${creds#*:}
DBPASS=$(printf '%b' "${DBPASS//\%/\\x}")
rest=${stripped#*@}
DBHOST=${rest%%:*}
DBNAME=${rest##*/}
export MYSQL_PWD="$DBPASS"
mysql -h "$DBHOST" -u "$DBUSER" "$DBNAME"
ポイントは3つあります。
printf '%b'によるURLデコード。%40を\x40に置換してからprintfに通すと@に戻ります。bashの機能だけでデコードできるので、サーバーにPythonなどが無くても動きます- 切り出しはsedの正規表現よりbashのパラメータ展開(
${var%%@*}など)のほうが、記号だらけのパスワードに対して安全です。最初にsedで書いて失敗したのが冒頭のエラーです MYSQL_PWD環境変数で渡すと、コマンドラインにパスワードが載りません。psコマンドやシェル履歴に残らず、mysql: [Warning] Using a password on the command line interface can be insecure.の警告も出ません
なお、このデコードは%XX形式を戻すだけの簡易実装です。.envに正しくエンコードされたDATABASE_URLが書かれている前提では、これで十分でした。
本番調査では認証情報をローカルに持ち帰らない
ssh越しに本番DBを調べるときは、切り出しからmysql実行までをリモート側のbashで完結させると、パスワードが手元のターミナル履歴や作業ログに一切残りません。
ssh production 'bash -s' <<'REMOTE'
URL=$(grep "^DATABASE_URL=" /path/to/eccube/.env | head -1 | cut -d= -f2-)
stripped=${URL#mysql://}
creds=${stripped%%@*}
DBUSER=${creds%%:*}
DBPASS=${creds#*:}
DBPASS=$(printf '%b' "${DBPASS//\%/\\x}")
rest=${stripped#*@}
DBHOST=${rest%%:*}
export MYSQL_PWD="$DBPASS"
mysql -h "$DBHOST" -u "$DBUSER" example_db -e "SELECT ..."
REMOTE
ヒアドキュメントの開始を<<'REMOTE'とクォート付きで書くのがコツで、ローカル側で変数展開されずスクリプト全体がそのままリモートで実行されます。
接続情報が合っているはずなのにmysqlコマンドだけAccess deniedになったら、まずパスワードのURLエンコードを疑ってみてください。
EC-CUBEに関するお問い合わせ
[重要]現在公式にセキュリティサポートが切れていないPHPは8.1以上、MySQLは8.0以上で、対応しているEC-CUBEバージョンは4.2以上です。古いEC-CUBEを使っている方は適切なタイミングでバージョンアップをご検討ください。