fuka’s diary

A blog that shares my knowledge.

HTTPクエリーを正しく受け取る

HTTPクエリーを正しく受け取ることは重要です。
これには、ふたつの意味があります。

  • HTTPクエリーを破損なく受け入れる
  • HTTPクエリーをバリデートし受け入れる

私が攻撃者であれば、例えば画像ファイルを偽装して、SQLPHP、時には別のコードのインジェクションを試みます。
添付ファイルがディレクトリーに保存された後、そのファイルを実行してデータを盗み見るのです。
設計によっては、保存するディレクトリーすら攻撃者が任意で変更できてしまう場合もあります。

あるいは、クエリー文字列内に悪意あるコードを忍ばせ、クロスサイトスクリプティングを試みます。
もしかしたら、これはコードインジェクションにもつながるかも知れません。
何度か試して、悪用の目途が立ったなら、ユーザー情報を盗み出すかも知れません。

このようなことになれば一大事、これら脆弱性を開発段階から撲滅する努力が、開発者には求められています。
この記事では、HTTPクエリーを正しく受け入れる知識を共有します。

想定する攻撃手法

 ファイル名にディレクトリー構造を書き加え、非公開ディレクトリーにアクセスします。

  • コードインジェクション

 プログラム言語や、データベース、時にはHTTPヘッダーに悪意あるコードを挿入します。

 悪意あるコードを送信し、利用者にコードを(手動、自動を問わずに)実行させます。
 厳密には異なりますが、コードインジェクションと一部が重複した攻撃と言えるでしょう。

  • ファイル偽装

 添付ファイルの拡張子やファイルヘッダーを偽装して、悪意あるファイルを受け入れさせます。

対策

POST/GETメソッド

基本的にユーザーが指定したディレクトリー構造や、ファイル名を受け入れないことが鉄則です。
ましてや、文字列をそのまま受け入れて、そのまま表示、などもってのほかです。

クエリー文字列を受け入れる時は、次のように置き換えます。

対象 置き換え後 備考
/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/

また、次の文字列は受け入れません。

対象 備考
/^javascript *:/ 大文字小文字を問わない

実際のコード例は以下の通りです。

<?php
function get_query_strings($name, $get_mode = false){
    if($get_mode){
        if(! isset($_GET[$name])){return false;}
        $text = $_GET[$name];
    }else{
        if(! isset($_POST[$name])){return false;}
        $text = $_POST[$name];
    }
    $text = preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
    $ret = false;
    if(isset($text)){
        if(is_array($text)){
            $ret = [];
            $values = $text;
            foreach ($values as $value){
                if(preg_match('/^ *javascript *:/i', $value) !== 0){return false;}
                $ret[] = $value;
            }
        }else{
            if(preg_match('/^ *javascript *:/i', $text) !== 0){return false;}
            $ret = $text;
        }
    }
    return $ret;
}

PHPで文字列を出力する時は、次のように置き換えます。

対象 置き換え後 備考
/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/
/^javascript *:/ 大文字小文字を問わない
& &amp;
' &#039;
&lt;
> &gt;

コード例が必要か微妙なところですが、コード例は次の通りです。

<?php
function sanitize($text){
    $text = preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
    $text = preg_replace('/^ *javascript *:/i', '', $text);
    $text = htmlspecialchars($text);
    return $text;
}

おまけですが、一部のHTMLタグのみ残したい場合があるかも知れません。
この場合は、次のように記述すると良いでしょう。

<?php
$code = "<STRONG>強調</strong>\n<EM>強調</em>\n<img src='sample.jpg'>\n<script>alert('アラート');</script>\n";
$result = preg_replace_callback(
    '/<(\/*)(.+?)>/i',
    function($match){
        $tags = ['strong', 'em'];
        $result = $match[0];
        if(array_search(mb_strtolower($match[2]), $tags) === false){
            $result = htmlspecialchars($match[0]);
        }
        return $result;
    },
    $code
);
echo $result;

上記は<STRONG>...</strong>と<EM>...</em>だけを残して、他のタグをHTMLエンティティーへ置き換えて(エスケープ処理をして)います。
アルファベットは大文字、小文字を問いません。

ファイル

どこまでを保護対象とするのか、サービス構築時にこれを考えることは重要です。
サービス側は前述の「攻撃手法」への対策を行います。
ユーザーの保護は、各ユーザー環境のソフトウェアに任せましょう。

ファイルのアップロード処理は、どのように行われているのでしょうか。
流れを追ってみましょう。

①アップロードされたファイルは、一時ファイルとしてディレクトリーに格納される
この時、ファイル名は個別に与えられます。
元々のファイル名や拡張子は失われているので注意しましょう。

②アップロードされたファイルの情報を受け取る
ファイル自体ではなく、アップロードされたファイルのエラー情報、格納先、元のファイル名、ファイルサイズなどを受け取ります。

③アップロードされたファイルを見つける
どこに、どのようなファイル名で格納されているか調べます。

MIMEタイプによる受け入れ検査
受け入れて良いファイルフォーマットかを調べます。
しばしば、攻撃者は拡張子を偽装しますので、間違っても拡張子だけで判別しないようにしましょう。

⑤イメージファイルであれば、同一ファイルフォーマットへ変換
画像を再変換することで、画像内に仕組まれた悪意あるコードを削除します。
変換できないことによってファイル偽装を検出できます。

⑥新しい名前の生成
ファイル名(ディレクトリーのパス)が悪用される可能性を排除します。

⑦ファイルを新しい名前で移動
アップロードされたファイルを受け入れます。
この時、決してアップロードしたユーザーが、保存先ディレクトリーを指定できるようにしてはいけません。
ファイルの格納先は、サービス側が「絶対パス」で決定しましょう。

パーミッションを変更
Windowsでは馴染みが薄いですが、ファイルの読み・書き・実行権限を設定します。
実行権限を解除することで、コードとして実行されることがなくなります。
格納ディレクトリーのパーミッションも正しく設定しましょう。

以下は実際のコード例です。

アップロードされたファイルの情報を受け取り、成功したファイルの情報を取得します。

<?php
function get_file($name){
    if(isset($_FILES[$name])){
        $ret = false;
        if(is_array($_FILES[$name]['name'])){
            $ret = [];
            $files = $_FILES[$name];
            $errors = $files['error'];
            $pos = 0;
            foreach ($errors as $error){
                if($error === UPLOAD_ERR_OK){
                    $dict = [];
                    $dict['name'] = $files['name'][$pos];
                    $dict['size'] = $files['size'][$pos];
                    $dict['tmp_name'] = $files['tmp_name'][$pos];
                    $ret[] = $dict;
                }else{
                    return false;
                }
            }
        }else{
            $file = $_FILES[$name];
            $ret = [];
            if($file['error'] === UPLOAD_ERR_OK){
                $ret['name'] = $file['name'];
                $ret['size'] = $file['size'];
                $ret['tmp_name'] = $file['tmp_name'];
            }else{
                return false;
            }
        }
        return $ret;
    }else{
        return false;
    }
}

アップロードされたファイルを受け入れ検査し、pngjpeg、gif画像は変換し、その他のファイルは移動します。
この際、パーミッションを644に設定し、不必要な書き込みと、実行権限を剥奪します。
受け入れるファイルは、MIMEタイプと拡張子(または、元ファイル名の拡張子を示すtrue)の連想配列で指定します。
標準では、pdf、gif、jpg、pngsvg(拡張子は.svgと.svgz)、mp4を受け入れます。

<?php
function create_file_name($directory, $extension){
    while(true){
        $file = date("YmdHis").bin2hex(random_bytes(32)).'.'.$extension;
        if(! file_exists($directory.'/'.$file)){break;}
    }
    return $directory.'/'.$file;
}

$mime_types = [
    'application/javascript'=>['js'],
    'application/json'=>['json'],
    'application/msword'=>['doc'],
    'application/pdf'=>['pdf'],
    'application/vnd.ms-cab-compressed'=>['cab'],
    'application/vnd.ms-excel'=>['xls'],
    'application/vnd.ms-outlook'=>['msg'],
    'application/vnd.ms-powerpoint'=>['ppt'],
    'application/vnd.oasis.opendocument.spreadsheet'=>['ods'],
    'application/vnd.oasis.opendocument.text'=>['odt'],
    'application/vnd.openxmlformats-officedocument.presentationml.presentation'=>['xlsm', 'xlsx'],
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'=>['docm', 'docx'],
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document'=>['pptm', 'pptx'],
    'application/x-lzh-compressed'=>['lzh'],
    'application/xml'=>['xml'],
    'application/x-msaccess'=>['accdb', 'mdb'],
    'application/x-msdownload'=>['exe', 'msi'],
    'application/x-rar-compressed'=>['rar'],
    'application/x-shockwave-flash'=>['swf'],
    'application/zip'=>['3mf', 'zip'],
    'audio/mpeg'=>['mp3'],
    'image/bmp'=>['bmp'],
    'image/gif'=>['gif'],
    'image/jpeg'=>['jpe', 'jpeg', 'jpg'],
    'image/png'=>['png'],
    'image/svg+xml'=>['svg', 'svgz'],
    'image/tiff'=>['tif'],
    'image/tiff'=>['tiff'],
    'image/vnd.microsoft.icon'=>['ico'],
    'text/css'=>['css'],
    'text/html'=>['htm', 'html', 'odc', 'php'],
    'text/plain'=>['bas', 'bat', 'cls', 'csv', 'ini', 'php', 'txt', 'vbs'],
    'video/mp4'=>['mp4'],
    'video/quicktime'=>['mov', 'qt'],
    'video/x-flv'=>['flv'],
];

function move_upload_file($from, $original_name, $to, $mime_extension = null){
    global $mime_types;
    try{
        if(! is_uploaded_file($from)){throw new ErrorException('Upload file does not exist.');}
        if(is_null($mime_extension)){
            $mime_extension = [
                'application/pdf'=>'pdf',
                'image/gif'=>'gif',
                'image/jpeg'=>'jpg',
                'image/png'=>'png',
                'image/svg+xml'=>true,
                'video/mp4'=>'mp4',
            ];
        }
        $mime = mime_content_type($from);
        if(! isset($mime_types[$mime])){throw new ErrorException('MIME type is undefined.');}
        $reasonable = false;
        foreach ($mime_types[$mime] as $extension){
            if(strrpos($original_name, '.') === false){throw new ErrorException('File name has no extension.');}
            if(strcmp(substr($original_name, strrpos($original_name, '.') + 1), $extension) === 0){
                $reasonable = true;
                break;
            }
        }
        if(! $reasonable){throw new ErrorException('Extension mismatch.');}
        $file_move = false;
        if(! isset($mime_extension[$mime])){throw new ErrorException('Not an acceptable MIME type.');}
        $ext = $mime_extension[$mime];
        if($ext === true){
            $ext = $extension;
        }
        $file = create_file_name($to, $ext);
        if(strcmp($mime, 'image/png') === 0){
            $image = imagecreatefrompng($from);
            if(! imagepng($image, $file)){throw new ErrorException('Image export failed.');}
        }elseif(strcmp($mime, 'image/gif') === 0){
            $image = imagecreatefromgif($from);
            if(! imagegif($image, $file)){throw new ErrorException('Image export failed.');}
        }elseif(strcmp($mime, 'image/jpeg') === 0){
            $image = imagecreatefromjpeg($from);
            if(! imagejpeg($image, $file)){throw new ErrorException('Image export failed.');}
        }else{
            $file_move = true;
        }
        if($file_move){
            if(! move_uploaded_file($from, $file)){throw new ErrorException('Failed to move the uploaded file.');}
        }else{
            if(! unlink($from)){throw new ErrorException('Failed to delete temporary files.');}
        }
        if(! chmod($file, 0644)){throw new ErrorException('Failed to change permissions.');}
    }catch(\Throwable $e){
        @unlink($from);
        $file = false;
    }
    return $file;
}

使い方は以下のとおりです。
ここでは、png、jpg、gifのみ受け入れています。

<?php
$files = get_file('test');
$from = $files[0]['tmp_name'];
$original_name = $files[0]['name'];
$to = './upload';
$mime_list = [
    'image/gif'=>'gif',
    'image/jpeg'=>'jpg',
    'image/png'=>'png',
];
echo move_upload_file($from, $original_name, $to, $mime_list);

Session機構を理解する

この記事では、Session機構の知識を共有します。
Session機構は独自に実装しない場合でも、仕組みを知ることは大切です。

これは『良し悪しの議論ではない』ことを前提にした話ですが、PHPが提供するSession機構では以下の機能が提供されません。
・Session IDの失効
・バリデートチェック

SessionファイルはApache2などのwebサーバーが管理しており、サーバー側によって定期的に削除されるため、Session IDの失効はPHPのコード上からは操作できません。
バリデートチェックは、確かにSessionファイルがサーバーのweb公開ディレクトリー以外に保存されるため改竄の危険は低く、必要ないと言えるかも知れません。
しかし、これらの機能は独自の実装では必要不可欠ですので、注意しましょう。
また、独自に設置したコンピューターでPHPが提供するSession機構を用いている場合には、Sessionファイルのメンテナンスを意識するようにしてください。

Session機構とは

一言で言うと「ユーザー認証を簡略化した仕組み」です。

分かり易く書くなら、
クライアントから「金庫室へ入りたい」と要求があった場合、サーバー側で金庫の暗証番号を用意し、最新の暗証番号をユーザーに教えます。
暗証番号はしばらくは有効ですが、一定時間を超えると失効します。
ユーザーは、Sessionが続く限り、その金庫に暗証番号でログインすることができます。

考え方

下図がSession機構の基本的な動作です。
ここでは、分かり易くするためSessionデータの記述は省いています。

Session ID

サーバーが発行するSession ID、つまり暗証番号です。
クライアントからの要求の都度、Session IDを新たに発行します。

構成 日時と、一意かつ予測が困難な文字列
長さ 日時14桁の数字+ランダム値32バイトのhex値
妥当性 ^\d{14}[0-9a-z]{64}$
有効期限 30分

一度利用したSession IDは直ちに無効化するのではなく、しばらく残しておきましょう。
そうしないと、不安定なネットワーク環境下でページ遷移に失敗した時や、同時アクセスによって、Sessionの継続性が保証できなくなります。

なお、Session IDはディスク資源を消費するため定期的に削除が必要です。
加えて、Session情報をGET/POSTメソッドで受け渡す設計にはしないようにしましょう。

上記ではSession IDに日時を使用しています。
構造を全体的に見て堅牢であれば問題なく、かつ一意性を向上させる意図があります。

例えば、1日10人が使用するシステムで、0~99までの数値で構成されたSession IDを使用するとします。
この場合、最大10日でSession IDは消費し尽くされてしまいます。

一方、Session IDに日付を連結した場合はどうでしょうか。
この場合、理想的にはSession IDは消費し尽くされることはありません。

これと同じ考え方で、日時とランダム値を組み合わせることで一意性が向上するわけです。

ディレクトリー

もし、レンタルサーバーがweb公開用のディレクトリーのみを提供している場合、.htaccessファイルなどでディレクトリー構造へのアクセス制御が必要です。
さもないと、攻撃者にSessionファイルにアクセスされてしまうかも知れません。
web公開用のディレクトリーの上位階層であるユーザーディレクトリーにアクセスできるなら、ここにSession管理用のディレクトリーを用意しましょう。

Sessionデータ

Sessionが持つ、Sessionが続く限り持つことができるデータです。
例えば、カートの中身などが想定されます。
Session機構の思想にもよりますが、これはSessionが開始され、終了するまでの一時的なデータです。

Session Cookie

Session Cookieはクライアント側のブラウザーが持つSession IDです。
DomainとPathを正しく指定し、以下の設定を基本とします。

SameSite Lax
Secure 有効
HttpOnly 有効
Expires 0(ブラウザー終了まで有効)

もし、通信内容を保護できない場合、Secureを無効とし、SameSiteはLaxかStrictとします。
この場合、通信内容の傍受によってSession ID漏洩のリスクが常に内在することに留意しましょう。

運用形態

ファイル名

ファイル名を用いてSession機構を実装します。
ファイル名の長さと文字種の制限を受けますが、比較的容易に実装できます。
ただし、ファイル数に上限がある場合は注意してください。

データベース

データベースを用いてSession機構を実装します。
気を付ないとサーバーに負荷をかける場合がありますが、長く複雑なSession IDを利用できます。
テーブル構造は次のようなものが考えられます。

カラム
PRIMARY KEY sessionid CHAR(78)
validity tinyint(1)
expiry DATETIME
sessiondata CHAR(78)

【以下は、DBMSに詳しい人向けの話です】
Session IDを格納するカラムは主キーにするのが適切です。
わざわざ主キー用にSession ID以外のキーを割り当てる必要はありません。
なぜなら、Session IDは十分にユニークであるためです。
さらに、Session IDにnullは期待しないため、主キー以外を選択する理由がないのです。

主キーにSession IDを用いると、「いずれインデックスが劣化して検索が遅くなるのではないか?」と心配になるかも知れません。
しかし、インデックスの劣化は多くが迷信で、大抵は「インデックスとは、B-tree走査と等価である」と言う誤解によって生じています。

インデックスは、以下の3ステップを辿ります。
①B-treeの走査
②リーフノードチェーン
③データ取り出し

確かにB-treeの階層が深くなれば、①に時間を要することになりますが、その値は小さいものです。
それ以上に、②と③で多くのアクセスが発生するとインデックス検索の速度は著しく低下します。
これがインデックスが劣化したように見える原因となります。
※例えば、重複値がある場合や、全値検索では多くの場合でリーフノードチェーンを辿ることになり、データ取り出しも複数回生じることによってレスポンスがさらに低下します。

インデックスに不適なテーブル構造はレスポンスを低下させますが、ユニークなSessionではそれほど懸念しなくて良いのです。
テーブル設計時、Session IDの他に、Sessionデータ格納や、ユーザー認証やらを融合させて「ゴッドテーブル」を作り込まないように注意しましょう。
ゴッドテーブルは管理を煩雑にするだけでなく、データベースのレスポンスを低下させる原因を作り込んでしまう可能性があります。

自動ログイン

無理に自動ログインを実装する必要はありませんが、基本的にSession機構と同じ仕組みを用います。
自動ログイン専用のSessionとCookieを用意することで、Session IDのメンテナンスと切り分けることが可能です。

違いは有効期間が長いこと、最低でも2つ以上のCookieを持つことです。
Cookie1つの強度は16^64ですが、2つの強度は16^128であり、より堅牢です。
Cookieの数に比例して強度は上がりますが、長すぎるSession IDはオーバークオリティーとなるので注意が必要です。
また、重要なサービスの場合、自動ログインを使わないか、有効期間を適切な値にしましょう。

なお、Session機構にも言えることですが、Cookieを盗まれるとアウトです。
しかし、適切なドメインを指定し、httponlyが有効なCookieが盗み出せるような脆弱な環境では、どんな情報も保護できません。

使用機会にストイックになり過ぎない

Sessionは画像やPDFファイルなどのデータでも作動できます。
しかし、もし画像にもSession機構を適用している場合、100ファイルの画像を表示するページでは、100回のSession認証が行われることになります。
何でもかんでもSession管理の対象としないよう、ターゲットを限定して運用しましょう。
画像やPDFは例えば別のサーバーへは位置し、リファラーでアクセス制御する方法が考えられます。

Outlook VBAによるメール作成の基礎

Outlook VBAの情報はそれほど多くないため、メール作成に限定して基礎となる情報を共有します。

メールオブジェクトを生成

Dim mail As MailItem: Set mail = CreateItem(olMailItem)

メールのInspectorを表示

mail.Display

送信アカウントを指定

アカウント名で指定

mail.SendUsingAccount = Session.Accounts("Account Name")

アカウント名をメールアドレスで指定

Dim acco As Account
For Each acco In Application.Session.Accounts
    If acco.CurrentUser.Address = "user@example.local" Then
        mail.SendUsingAccount = acco: Exit For
    End If
Next acco

Subject

mail.Subject = "Mail title"

To

mail.To = "test1@example.local; test2@example.local; test3 <test3@example.local>"

CC

mail.CC = "test1@example.local; test2@example.local; test3 <test3@example.local>"

BCC

mail.BCC = "test1@example.local; test2@example.local; test3 <test3@example.local>"

各フォーマットによるメール作成

テキスト

mail.BodyFormat = olFormatPlain
mail.Body = "test"

HTML

mail.BodyFormat = olFormatHTML
mail.HTMLBody = "<b>test</b>"

RTF

mail.BodyFormat = olFormatRichText
' RTF表記は複雑のためここでは省略します。
' なお、RTFはOffice 365/2019ではUTF-32のバイト列であることを確認しています。

添付ファイル

追加

mail.Attachments.Add "c:\test\sample.txt", olByValue
mail.Attachments.Add "c:\test\sample.msg", olEmbeddeditem

削除

mail.Attachments.Remove 1

下書き保存

mail.Save

送信

mail.Send

PHPにおけるプリペアドステートメントの使用

プリペアドステートメントとは、SQLの可変部分を変数のように扱う方法です。
可変部分に与えられる値を、安全にSQL構文に組み込むことが可能です。
つまり、SQLインジェクションを防げるわけですね。

ただし、いくつか分かりづらい仕様があるため、知識を共有します。
それは、属性と型の指定です。

まずはコードを見てみましょう。
MITライセンスね。

<?php
function sql($dbname, $hostname, $user, $passwd, $bind_array, $sql_command, $get_return = false){
    $db = 'mysql:dbname='.$dbname.';host='.$hostname.'';
    try{
        $pdo = new PDO($db ,$user, $passwd, [PDO::ATTR_PERSISTENT => false]);
        $pdo->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_NATURAL);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        //$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
        $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        if(! $pdo->beginTransaction()){
            throw new ErrorException('Transaction failed.');
        }
        $stmt = $pdo->prepare($sql_command);
        foreach ($bind_array as $key => $value){
            $type = null;
            $typelist = [
                ':bool_' => PDO::PARAM_BOOL,
                ':int_' => PDO::PARAM_INT,
                ':float_' => PDO::PARAM_STR,
                ':str_' => PDO::PARAM_STR,
                ':blob_' => PDO::PARAM_LOB,
                ':null_' => PDO::PARAM_NULL,
            ];
            foreach ($typelist as $header => $typevalue){
                if(strcmp(substr($key, 0, strlen($header)), $header) === 0){
                    $type = $typevalue;
                    break;
                }
            }
            if(is_null($type)){
                throw new ErrorException('Type cannot be identified.');
            }
            if(! $stmt->bindValue($key, $value, $type)){
                throw new ErrorException('Binding failed.');
            }
        }
        $resp = $stmt->execute();
        if($resp){
            $pdo->commit();
        }else{
            throw new ErrorException('Execute failed.');
        }
        $result = $resp;
        if($get_return){
            $result = $stmt->fetch();
        }
    }catch(\Throwable $e){
        $result = false;
    }
    $pdo = null;
    return $result;
}

上記のコードはPDOを用いてMySQLのDBエンジンと通信します。
ポイントは、$stmt->fetch();を戻り値取得時にしか実行しないこと、catch時に$pdo->rollBack();を記述しないことです。

エミュレーションモードが有効である場合、戻り値のないコマンドにfetchを試みると、一般エラーになります。
この後、失敗しているにも関わらずロールバックを試みてしまうと、さらなるエラーを誘発します。
一方、エミュレーションモードが無効である場合、戻り値のないコマンドにfetchを試みるとfalseが戻ります。

呼び出し方は以下のとおりです。

<?php
error_reporting(E_ALL);
//error_reporting(0);
$result = sql('dbt',
    'localhost',
    'username',
    'password',
    [
        ':int_num'=>1,
        ':str_text'=>'文字列',
        ':str_date_time'=>'2021-01-01 10:10:10',
        ':str_num_float'=>0.1,
        ':bool_bool'=>true,
        ':blob_large'=>'テスト'
    ],
    'insert into table1 (num, text, date_time, num_float, bool, large) values (:int_num, :str_text, :date_date_time, :float_num_float, :bool_bool, :blob_larg'
);
if($result === false){
    echo 'Failure';
}

それでは、属性と型の指定を読み解いていきましょう。

属性

データベース接続の持続: しない

<?php
$pdo = new PDO($db ,$user, $passwd, [PDO::ATTR_PERSISTENT => false]);

データベース接続をインスタンス化し持続的(永続的)に接続します。
規模が大きなサービスでは有効ですが、レンタルサーバーでの運用ではfalseが良いでしょう。

上記の[PDO::ATTR_PERSISTENT => false]の部分が該当の属性です。
時折setAttributeでPDO::ATTR_PERSISTENTを記述した例を見ることがありますが、PDO初期化の段階で指定しないと、この機能は働きませんので注意しましょう。

nullと空白文字の変換: しない

<?php
$pdo->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_NATURAL);

ここでは、nullや空白文字の変換は行わないようにしています。

PDO::NULL_NATURAL 変換なし
PDO::NULL_EMPTY_STRING 空文字をnullにする
PDO::NULL_TO_STRING nullを空文字にする

エラーレポート: 例外を発生

<?php
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

開発中は例外をthrowするようにしましょう。
運用中はエラーレポートの出力は脆弱性につながりますので、エラーコードのみ(サイレント)にします。

<?php
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
PDO::ERRMODE_SILENT エラーコードのみ
PDO::ERRMODE_WARNING E_WARNING(警告)を発生
PDO::ERRMODE_EXCEPTION 例外を発生

Fetchモード: 連想配列として取得

<?php
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

取得するデータは連想配列にすると便利です。
モードは以下があります。

モード 概要
PDO::FETCH_BOTH カラム名連想配列と配列の混合(デフォルト)
PDO::FETCH_NUM 配列
PDO::FETCH_ASSOC カラム名連想配列
PDO::FETCH_NAMED PDO::FETCH_ASSOCと同じ。ただし、同一名カラムを結合
PDO::FETCH_BOUND bindColumn()でバインドされた変数に代入
PDO::FETCH_INTO 既存インスタンスに代入
PDO::FETCH_CLASS 新規クラスに代入
PDO::FETCH_PROPS_LATE PDO::FETCH_CLASSと共に呼び出すと、コンストラクターを呼び出してからプロパティーを代入
PDO::FETCH_COLUMN カラム名のみ、0ですべて、それ以外で指定カラム
PDO::FETCH_OBJ カラム名をプロパティーとするstdClassオブジェクト
PDO::FETCH_LAZY カラム名をプロパティーとするPDORowオブジェクト

プリペアドステートメントのエミュレーション: 無効

<?php
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

エミュレーションモードが無効の場合、静的プレースホルダーと呼ばれる「DBエンジンがネイティブにサポートするプリペアドステートメント」を用います。
エミュレーションモードが有効の場合、動的プレースホルダーと呼ばれる、「ライブラリーが独自に実装したプリペアドステートメント」を用います。

プリペアドステートメントのエミュレーションは過去にはライブラリーの実装に不具合があり、SQLインジェクション脆弱性が生じていました。
エミュレーションの脆弱性はここのところ報告がありませんが、この歴史的背景をどう捉えるかがポイントです。

個人的には無効にしますが、サーバー資源や環境、または思想によって選択はそれぞれです。
よって、以下のどちらを選択するか、論点はここに帰結します。

 同一名のプレースホルダー、複文の実行が可能です。

 レスポンスはやや低下しますが、ライブラリー実装の不具合による脆弱性を回避できます。

まずはコードを見てみましょう。

<?php
$typelist = [
    ':bool_' => PDO::PARAM_BOOL,
    ':int_' => PDO::PARAM_INT,
    ':float_' => PDO::PARAM_STR,
    ':str_' => PDO::PARAM_STR,
    ':blob_' => PDO::PARAM_LOB,
    ':null_' => PDO::PARAM_NULL,
];

PDOで指定可能な型はBOOLEAN、INT、STR、BLOB、NULLのみです。
FLOATをはじめ、DATEやDATETIME、VCHARやCHARなどの型はありません。
その他の型に対して、PDOではSTR型を指定します。

では、fetchの結果を見てみましょう。
以下のようにデータを取得します。

<?php
$result = sql(
    'dbt',
    'localhost',
    'username',
    'password',
    [],
    'SELECT * FROM `table1` WHERE id=0',
    true
);
var_dump($result);

エミュレーション無効

<?php
array(7) {
    ["id"]=> int(0)
    ["num"]=> int(100)
    ["text"]=> string(9) "文字列"
    ["date_time"]=> string(19) "2021-01-01 10:10:10"
    ["num_float"]=> float(0.1)
    ["bool"]=> int(1)
    ["large"]=> string(9) "テスト"
}

PDOはテーブル側の型指定に従い、可能な限りデータを取得しようとしているのが分かりますね。
ただし、PHPではバイナリはString型、日時はDateTimeクラスによって実装しています。
このため、どうしようもないものは、String型として取得しています。
なお、MySQLでは内部的にBOOLEAN型をTINYINTとして扱っています。
PHPもこれに従ってint型に変換しているわけです。

エミュレーション有効

<?php
array(7) {
  ["id"]=>  string(2) "0"
  ["num"]=>  string(3) "100"
  ["text"]=>  string(9) "文字列"
  ["date_time"]=>  string(19) "2021-01-01 10:10:10"
  ["num_float"]=>  string(3) "0.1"
  ["bool"]=>  string(1) "1"
  ["large"]=>  string(9) "テスト"
}

エミュレーションが有効の場合、取得する値はすべてString型に変換されています。
型はすべてString型であれ、内容はエミュレーション無効と同じですね。

余談ですが、BLOB型は「大きいサイズ」と表現していますが、これは4KB~32KB以上と定義はデータベースによって異なります。
そして、「大きいサイズ」は運用環境、設計思想によっても異なります。
個人的には、MySQLでは、VARBINARY型のサイズ指定をしないものをBLOBと捉えています。
用途は画像などのバイナリーが想定されます。

MySQLのTEXT型もBLOB型と似たことが言えるかも知れません。
つまり、私はMySQLでは、VARCHAR型のサイズ指定をしないものをTEXT型と捉えています。
これはテキストドキュメントや長文の投稿などが想定されます。

【おまけ】bindValueとbindParam

bindValueは値がバインド(紐づけ)されます。
bindParamは値ではなく、変数のリファレンス渡し(参照渡し)がバインドされます。
よって、bindParamはexecuteが実行された時点での変数内容が採用されます。
bindParamではリテラルが扱えない一方、データ型の長さを指定することが可能です。
なお、個人的には脆弱性を書き込まないためにbindValueを用いるように心がけています。

PHPでのMIMEタイプ取得にはmime_content_typeを使いましょう

PHPにおけるファイルのMIMEタイプ取得にはmime_content_typeを使いましょう。
一部で非推奨とする誤った情報が根強いようですが、これは2016年までの話です。
今も一部のドキュメントでは非推奨と表示されていますが、それは誤りです。

mime_content_typeが取得する代表的なMIMEタイプを下記に列挙します。

<?php
$mime_types = [
	'application/javascript'=>['js'],
	'application/json'=>['json'],
	'application/msword'=>['doc'],
	'application/pdf'=>['pdf'],
	'application/vnd.ms-cab-compressed'=>['cab'],
	'application/vnd.ms-excel'=>['xls'],
	'application/vnd.ms-outlook'=>['msg'],
	'application/vnd.ms-powerpoint'=>['ppt'],
	'application/vnd.oasis.opendocument.spreadsheet'=>['ods'],
	'application/vnd.oasis.opendocument.text'=>['odt'],
	'application/vnd.openxmlformats-officedocument.presentationml.presentation'=>['xlsm', 'xlsx'],
	'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'=>['docm', 'docx'],
	'application/vnd.openxmlformats-officedocument.wordprocessingml.document'=>['pptm', 'pptx'],
	'application/x-lzh-compressed'=>['lzh'],
	'application/xml'=>['xml'],
	'application/x-msaccess'=>['accdb', 'mdb'],
	'application/x-msdownload'=>['exe', 'msi'],
	'application/x-rar-compressed'=>['rar'],
	'application/x-shockwave-flash'=>['swf'],
	'application/zip'=>['3mf', 'zip'],
	'audio/mpeg'=>['mp3'],
	'image/bmp'=>['bmp'],
	'image/gif'=>['gif'],
	'image/jpeg'=>['jpe', 'jpeg', 'jpg'],
	'image/png'=>['png'],
	'image/svg+xml'=>['svg', 'svgz'],
	'image/tiff'=>['tif'],
	'image/tiff'=>['tiff'],
	'image/vnd.microsoft.icon'=>['ico'],
	'text/css'=>['css'],
	'text/html'=>['htm', 'html', 'odc', 'php'],
	'text/plain'=>['bas', 'bat', 'cls', 'csv', 'ini', 'php', 'txt', 'vbs'],
	'video/mp4'=>['mp4'],
	'video/quicktime'=>['mov', 'qt'],
	'video/x-flv'=>['flv'],
];

ちなみに、上記に含みませんでしたが、扱いがありそうなファイルについても表にします。

MIME type Extension
application/postscript ai
application/postscript eps
application/postscript ps
application/rtf rtf

オブジェクト指向プログラミングとは何か

オブジェクト指向プログラミングとは何か?
ある人は「多態性」、ある人は「多重定義(オーバーロード)」、ある人は「依存性の注入」として、オブジェクト指向プログラミングを説明するでしょう。
継承、仮想・抽象メンバー、オーバーライド、カプセル化など、オブジェクト指向プログラミングとは様々なものである、とも言えます。
「オブジェクトをモノに例える」説明は、オブジェクト指向プログラミングのクラスとメンバー、そして時には継承を説明したものです。

この記事ではオブジェクト指向プログラミングの用語とその意味の知識を共有します。
そして、「では、お前はオブジェクト指向プログラミングを何だと思っているのか?」を書かせて頂きます。

オブジェクト

大雑把に言えば、オブジェクト指向プログラミングで用いる様々な機能の総称です。
しかし、正確には総称とするのは誤りです。
データと機能の集合としての実体であり、個としての実体であり、プログラム言語によってもニュアンスが異なる場合がありますが、これらをオブジェクトとして捉えることも大切な考え方であるためです。
そして、アプリケーションや文書においてもオブジェクトとは、それぞれの環境下で意味するものが異なります。
目的や環境に応じてオブジェクトが指す機能や事柄、あるいは意味が異なる点に注意しましょう。

クラス

クラスとはインスタンス化されていない状態を意識する場合の用語です。
設計図とも言われますが、テンプレートであるとも言えます。
または、データと機能の集合としての実体とも言えます。

抽象クラス

抽象メンバーを持つクラスです。
人によっては「アブストラクトクラス」と呼ぶ場合がありますか、一般的ではありません。

スーパークラス

継承元となる親のクラスです。
基底クラス、親クラスとも呼ばれます。

サブクラス

継承先となる子のクラスです。
派生クラス、子クラスとも呼ばれます。

インスタンス

インスタンスは、クラスをインスタンス化した状態を意識する場合の用語です。
実体とも言われますが、「クラスを元に生成された、実際に動作させることが可能な状態」とも言えます。
データと機能の集合としての実体に基づいて生成された、個としての実体とも言えます。

メンバー

クラスやインスタンスが持つメソッド、プロパティー、フィールド等の要素の総称です。

仮想メンバー

仮想メソッドと仮想プロパティーを指します。
プログラム言語によって異なりますが、オーバーライドすることを前提としています。
言い換えると、規定の実装とも表現できます。

抽象メンバー

抽象メソッドと抽象プロパティーを指します。
この抽象メンバーを含むクラスを「抽象クラス」と呼びます。
この抽象クラスを継承した子クラスでは、必ず抽象メンバーを定義しなくてはなりません。

インターフェース

インターフェースは単一継承の言語で良く使用されます。
インターフェースは多重継承が許されているためです。
抽象クラスは内部にコードの存在を許しますが、インターフェースは許しません。

つまり、インターフェースと抽象クラスとは、パソコンを例にするなら、キーボードやマウスの存在を示すものと説明できます。
インターフェースは、パソコン本体とディスプレイを別途用意して、マウスやキーボードを接続します。
抽象クラスは、パソコン本体とディスプレイは予め用意しておいて、マウスやキーボードを接続するイメージです。
【注記】
 上記の説明も十分ではありません。
 インターフェースは機能定義、抽象クラスは機能実装定義であり、故にインターフェースの設計は非常にシビアです。
 この絶妙なニュアンスは実際に言語を学ぶ中で感覚を掴んでいけるでしょう。

ちなみにJavaは単一継承言語のため、インターフェースを利用する機会が多くあります。
このため用語として抽象クラスをインターフェースと呼んでいる場合があるので注意しましょう。

メソッド

機能に対するアクセス制御です。
これは、関数と言うこともできますが、クラスメソッド、インスタンスメソッド、仮想メソッド、抽象メソッドである点を考えると、単純な関数とは表現できません。
つまり、メソッドはメソッドなのです。

クラスメソッド

クラスからインスタンスを生成せずに、例えば Class.Method() のように直接呼び出すメソッドです。

インスタンスメソッド

Instance.Method()のように、インスタンスから呼び出すメソッドです。

プロパティー

データに対するアクセス制御です。
これは、変数と言うこともできますが、仮想プロパティー、抽象プロパティー、ミューテーター、アクセサー、非対称アクセス、自動実装、あるいは、メソッドと似た実装でもあります。
つまり、プロパティーはプロパティーなのです。

フィールド

メンバー変数とも呼ばれます。
オブジェクトが持つデータであり、インスタンスからアクセスします。
プロパティーは「アクセス制御の機能」であり、フィールドは「アクセスされるデータ」です。
オブジェクト内の変数と表現できますが、メンバー変数(インスタンスが持つプライベート変数)と説明した方が、より正確でしょう。

スコープ

オブジェクト指向プログラミングに限った話ではありませんが、参照範囲の名前空間に対する振る舞いです。
関数や変数はどこから見える?と考えてしまっても(少し語弊はありますが)構わないでしょう。

Pythonではグローバルスコープと、ローカルスコープの2種類があります。
グローバルスコープはすべての関数やクラスから参照できますが、使用するにはglobal文でグローバル変数を定義しなくてはいけません。
ローカルスコープは関数やクラス、またはそのメンバー内でのみ参照できます。
ただし、クラスで定義したメンバー変数は、クラス内の他のメンバーからself.__varとして参照することができます。

コンストラクター

クラスがインスタンス化する時に実行されるメソッドです。

デストラクター

インスタンスが破棄されたか、スコープ外となった時に実行されるメソッドです。

イテレータ

データ集合(コンテナーやコレクション)の繰り返し処理において、各要素にアクセスします。

ジェネレーター

イテレーターを生成します。

インデクサー

インスタンスに対するメソッドの明示がなくとも実行されるメソッドです。

依存性の注入

DI(Dependency injection)と呼びます。
過去にはInversion of Control: IoC(制御の逆転)と呼ばれ、これは、「必要な制御を自分で揃える」と対である「必要な制御を外部から揃える」のようなイメージです。
開発者は依存関係がはっきりしたコードを読むことができるので、コンベンジョン(規則)に従うだけで他のクラスを意識する必要がなくなります。
しかし、気を付けないと、サービスロケーターと呼ばれるアンチパターンを書き込んでしまう場合もあります。

ファクトリー

オブジェクトを生成するメンバーです。
日本語では「工場」の意味です。
オブジェクトの生成を動的に行うことを「ファクトリーパターン」と呼び、オブジェクトの生成を行うメソッドを個別に用意することを「ファクトリーメソッドパターン」と呼びます。
これらの手法によって、「クラスからオブジェクト生成のメソッドを追い出す」ことが可能となり、クラスの再利用性を高めます。

コンテナー

オブジェクトを格納できるオブジェクトです。
コンテナーはコレクションと呼ばれるものと同じです。
コンテナーは「入れ物」の意味です。
コレクションとは「集めた物」の意味であり、実体を意識しているか、その内容を意識しているかがよく分かりますね。
データの入れ物として意識する場合はコンテナー、処理や機能などで用いるデータ集合として意識する場合はコレクションと呼び分けるわけです。

コンテナーを具体的に挙げると、Pythonではbytes, bytearray, dict, frozenset, list, range, set, str, tupleを指します。
けれども、DIで用いる場合のコンテナーとはオブジェクトを格納できる必要があるため、dict, list, set, tupleになります。

サービスロケーターとDI

ファクトリー「内」で使用するために注入されたコンテナーは「DI」です。
ファクトリー「外」で使用するために注入されたコンテナー、これを「サービスロケーター」と呼びます。
サービスロケーターは一般的にアンチパターンと認識されます。
開発者が意図してサービスロケーターとDIを混ぜこぜにして実装した場合、サービスロケーターでもDIでもありません。
それはオブジェクトのコンテナーであり、IoCです。

サービスロケーターがアンチパターンな理由

なぜ、ファクトリー外で使用するためにコンテナーを注入するのがダメなのか、それは一言で表現すると「分かりづらい」からです。
サービスロケーターは、開発者に他のクラスを意識させようとします。
そもそも、DIとは「依存するオブジェクトを明瞭にする」のではなく、「依存するオブジェクトを追い出して、分かりやすくする」ことが目的です。
サービスロケーターは「依存するオブジェクトをわざわざ取り込んで使用するため、分かりづらくなる」ために、アンチパターンと呼ばれるのです。

カプセル化

外部からデータを隠蔽し、データを保護するアクセス制御です。
隠蔽されたデータは外部から直接アクセスすることができませんが、操作したり閲覧が必要な場合はミューテーター(セッター)とアクセサー(ゲッター)と呼ばれるプロパティーを実装します。
カプセル化は積極的に行い、オブジェクトの設計情報や内部情報が漏洩しないように注意しましょう。

継承

同じ事を書かないように機能を整理し、オブジェクトに階層構造を持たせます。
継承元を「親」、継承した側を「子」と呼び、親は複数持つことができ、子も複数持つことができます。

ひし形継承問題

「Aを継承したBとCがある。BとCを多重継承したDは、BとC、どちらのメソッドを継承すべきか?」と言う問題です。
AからのBとCの分岐、それがDへ集約する形が「ひし形」と言うわけです。
これは、メソッド順序解決(Method Resolution Order: MRO)とも呼ばれます。
この解決法はプログラム言語毎に異なりますので注意しましょう。

Pythonでは、ひし形継承問題をC3線形化によって解決しています。

class A(object):
    def __init__(self):
        print('A')
        super().__init__()
class B(A):
    def __init__(self):
        print('B')
        super().__init__()
class C(A):
    def __init__(self):
        print('C')
        super().__init__()
class D(C, B):
    def __init__(self):
        print('D')
        super().__init__()

d = D()

出力は次のようになります。

D
C
B
A

まず、クラスDは、クラスC, Bの順に継承しています。
CとBは、クラスAを継承しています。
C3線形化はD→C→B→Aの順に解決していきます。

下記は、Pythonでのより実践的な例です。

class A(object):
    def __init__(self):
        print('start A')
        print('exit A')

class B(A):
    def __init__(self, b):
        print('start B')
        super().__init__()
        print('exit B')

class C(A):
    def __init__(self, c, **kwargs):
        print('start C')
        super().__init__(**kwargs)
        print('exit C')

class D(C, B):
    def __init__(self):
        print('start D')
        super().__init__(b=1, c=1)
        print('exit D')

d = D()

出力は次のようになります。

start D
start B
start C
start A
exit A
exit C
exit B
exit D

この時、BとCのコンストラクターの引数が異なる点に注目してください。
解決順序を追ってみましょう。

①Dはsuper()でCのコンストラクターにアクセスします。
 この時、キーワード引数で、b = 1とc = 1を明示しています。

②Cのコンストラクターは引数cを受け取ります。
 この時、**kwargsで可変キーワード引数{'b': 1}も受け取ります。

③Cはsuper()で、Bのコンストラクターにアクセスします。
 この時、Bのコンストラクターへ**kwargsで可変キーワード引数{'b': 1}を渡します。

④Bのコンストラクターは引数bを受け取ります。

⑤Bはsuper()で、Aのコンストラクターにアクセスします。

⑥Aのコンストラクターは引数を何も受け取りません。

多態性

ポリモーフィズムや、多相性とも呼ばれます。
同じ名前のメソッドでも、引数の型や個数に応じて、個別の機能にアクセスできることです。
多態性と多重定義(オーバーロード)をごちゃまぜにしないように注意しましょう。
これらは広い意味では関係性がありますが、狭い意味では別のものです。
多態性には次のような種類があります。

サブタイピング多相

引数の型として指定した型Aがあるとして、Aより派生したBも受け入れます。
オブジェクト指向プログラミングでは継承として実装され、オブジェクトAを継承したBを受け入れます。
必ずしも継承に限定されないことに注意してください。
多くの言語で多態性とは、サブタイピング多相を指します。

アドホック多相

同一名の関数やメソッドであっても、引数の型や数の違いに応じて、それぞれの実装を呼び出します。
オブジェクト指向プログラミングでは多重定義(オーバーロード)として実装されています。

パラメトリック多相

引数の型をパラメーターとして渡すことができます。
動的型付けの言語では意識することは少ないですが、型に依存しないプログラミングが可能となり、これを型の抽象化と呼びます。
例えば、文字列型の値を引数として与えた場合、関数側は文字列型の引数として受け入れます。
または、数値型の値を引数で与えた場合、関数側は数値型の引数として受け入れます。
ジェネリクスが該当します。

多重定義

オーバーロードとも呼ばれます。
引数の型や個数が異なる場合、同一名のメソッドを複数個持つことができます。
多態性と似ていますが、多態性は「使う側」の話で、多重定義(オーバーロード)は「動作する側」の話です。
ちなみに、PHPでのオーバーロードは「動的プロパティーやメソッドの生成」を意味しているので、まったくの別物です。

オーバーライド

継承した親クラスのメソッドと、子クラスのメソッドが名称、引数の型と数が同じである場合、子クラスのメソッドが呼び出されることです。
多重定義(オーバーロード)や多態性とは広い意味では関係性がありますが、別のものとして考えましょう。
単純な上書きとは異なるので誤解しないように注意しましょう。

【おまけ】ダック・タイピング

同一のメンバーを持つオブジェクトであれば、入れ替えて使用できるプログラミング手法です。
動的型付けが可能な柔軟なプログラム言語で利用できるテクニックです。

以下は、自分自身にアヒルと犬を憑依させ、自己紹介の上で鳴き声を出す例です。

class Duck(object):
    def __init__(self):
        print('I\'m a duck')
    def sound(self):
        print('quack quack !')

class Dog(object):
    def __init__(self):
        print('I\'m a dog.')
    def sound(self):
        print('bow wow !')

def Me(soul):
    soul.sound()

Me(Duck())
Me(Dog())

出力は次のようになります。

I'm a duck
quack quack !
I'm a dog.
bow wow !

オブジェクト指向プログラミングとは何か

私はオブジェクト指向プログラミングとは「目的を意識する姿勢」であると考えています。
特定の機能や手法を指すのではなく、メンバーとクラス、変数や関数の正確な命名を基礎として、DIによって依存するオブジェクトの目的を鮮明にして、正しい名前を付けた上で追い出すのです。
その上で、オブジェクトが持つ機能の目的を明瞭にして再利用性を高めます。

私の認識では、オブジェクト指向プログラミングとは一種の哲学であり、「これはナイス!」と言うテクニックや機能、運用性や知識と言った英知の蓄積です。
プログラマーがコーディングし、それが動くことは当然できることですが、「その先」の目的を意識させるのが、オブジェクト指向プログラミングなのではないでしょうか。
これらを、より平たく言うなら「思いやりを大切にしようプログラミング」になるかも知れません。

PHPにおけるAES暗号化アルゴリズムGCM/CBCモードの使用

PHPではopenssl_encrypt、openssl_decryptを用いて対称暗号化アルゴリズムを使用できます。
クセモノなのが、$options引数と、$iv引数です。
ここを誤ると脆弱性を書き込んでしまうので、知識を共有します。

$options引数

公式マニュアルにある通り、以下の定数が使用できます。

OPENSSL_RAW_DATA = 1

暗号化データを生データとして処理します。
なお、指定がない場合は、暗号化データをBASE64として処理します。

OPENSSL_ZERO_PADDING = 2

パディングされません。GCMモードで使用します。

時折、OPENSSL_NO_PADDINGを使用した例を見ることがありますが、使用可能な定数は上記の2つだけです。
OPENSSL_NO_PADDING = 3なので、OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDINGと等価ではあります。
しかし、OPENSSL_NO_PADDINGは非対称暗号で用いる定数ですから、対称暗号化では使用してはいけません。

$iv引数

初期ベクトルはnonceであることが求められます。
nonceとは一意値(重複しない値)であり、単純にランダム値だけで構成することは誤りです。
また、UNIXタイムでは完全同時アクセスに脆弱であり、UUIDは確率的に衝突はゼロではありません。
PHPにはopenssl_random_pseudo_bytes関数が用意されていますので、これを使いましょう。
ちなみに、インクリメント値もnonceとして妥当ですが、攻撃を受けやすい弱点がありますから注意してください。

実装

実装は次のようになります。
ライセンスはMITライセンスね。

<?php
function enc_aes_gcm($plain, string $passwd, bool $base64_mode = true){
	$key = hash('sha256', $passwd, true);
	$options = OPENSSL_ZERO_PADDING;
	if(! $base64_mode){
		$options = $options|OPENSSL_RAW_DATA;
	}
	$ivleng = openssl_cipher_iv_length('aes-256-gcm');
	$iv = openssl_random_pseudo_bytes($ivleng);
	$tag = null;
	$cipherdata = false;
	try{
		$cipherdata = openssl_encrypt(
			$plain,
			'aes-256-gcm',
			$key,
			$options,
			$iv,
			$tag
		);
	}catch(\Throwable $e){
		;
	}
	return ['data'=>$cipherdata, 'iv'=>$iv, 'tag'=>$tag];
}

function dec_aes_gcm($cipherdata, string $passwd, string $iv, string $tag, bool $base64_mode = true){
	$key = hash('sha256', $passwd, true);
	$options = OPENSSL_ZERO_PADDING;
	if(! $base64_mode){
		$options = $options|OPENSSL_RAW_DATA;
	}
	$plain = false;
	try{
		$plain = openssl_decrypt(
			$cipherdata,
			'aes-256-gcm',
			$key,
			$options,
			$iv,
			$tag
		);
	}catch(\Throwable $e){
		;
	}
	return $plain;
}

function enc_aes_cbc($plain, string $passwd, bool $base64_mode = true){
	$key = hash('sha256', $passwd, true);
	$options = 0;
	if(! $base64_mode){
		$options = OPENSSL_RAW_DATA;
	}
	$ivleng = openssl_cipher_iv_length('aes-256-cbc');
	$iv = openssl_random_pseudo_bytes($ivleng);
	$cipherdata = false;
	try{
		$cipherdata = openssl_encrypt(
			$plain,
			'aes-256-cbc',
			$key,
			$options,
			$iv
		);
	}catch(\Throwable $e){
		;
	}
	return ['data'=>$cipherdata, 'iv'=>$iv];
}

function dec_aes_cbc($cipherdata, string $passwd, string $iv, bool $base64_mode = true){
	$key = hash('sha256', $passwd, true);
	$options = 0;
	if(! $base64_mode){
		$options = OPENSSL_RAW_DATA;
	}
	$plain = false;
	try{
		$plain = openssl_decrypt(
			$cipherdata,
			'aes-256-cbc',
			$key,
			$options,
			$iv
		);
	}catch(\Throwable $e){
		;
	}
	return $plain;
}

実際に使う時はこんな感じです。

<?php
$password = 'aaaaa';
$plaintext = 'テスト';
$enc = enc_aes_gcm($plaintext, $password, false);
$dec = dec_aes_gcm($enc['data'], $password, $enc['iv'], $enc['tag']);
echo $dec;
$enc = enc_aes_cbc($plaintext, $password, false);
$dec = dec_aes_cbc($enc['data'], $password, $enc['iv']);
echo $dec;

exit();