milvinae

web & product design.

ウェブサイトにあげる3Dデータをスクレイピングから守るには?


やった!3Dデータを作ったぞ!
3Dプリントもできるし、モデルデータ販売サイトにも登録した!
よーし、サイトに公開してみんなに周知しよう!

………これ、配置しただけだとスクレイピングでデータ窃取できちゃうな…。



サイバー空間にアップしたいけど上げれない、そんなジレンマがあるかと思います。
今回は本サイトでも採用している回避方法を紹介します。


まずやりたいこととその問題を洗い出していきましょう。


<やりたいこと>
・3Dモデルをサイト上に公開したい。
・回転させたり拡大縮小させたりしてぐりぐり操作したい。
・パスは隠してDLできないようにしたい。
・サイトへの導入はシンプルに、使いまわせるようにしたい。

<問題>
・3Dビューアを作るのはコストがかさむ。
 → model-viewerを使う。
・Wordpressのメディアライブラリでは3Dモデルを扱えない。
・クライアント側(model-viewer)にモデルのパスを渡す形では、DLできてしまうしディレクトリトラバーサルされる危険性もある。


クライアント側(JavascriptとCSS)だけではちょっと解決が難しいかもしれません。
サーバ側のことはあんまりよくわからないので、こういう時はGPTちゃんに聞くことにします。

GPTちゃん、頼りになるわぁ。

ユーザ側に表示されている以上完全に防御することは不可能。
まぁそうだよね。
とはいえ、し難くすることはできそうな感じです。

アクセス制限やログイン制にするのは本末転倒なので、glTFストリーミングする方向で考えていきましょう。
ただし、分割処理や暗号複合、アクセス制限などはコスト高めなので、とりあえず”サーバから生データでモデルをもらう”という形にしておきましょう。


さて、全体の構成を考えていきましょう。



構成はいたってシンプル。
model-viewerはWordpressのカスタムURLで記述、データ問い合わせとViewerへのセットを担当するJS、サーバ側のデータ処理を担当するPHPで構成されています。

1.model-viewerが表示されるのをトリガーに、PHPにモデル名をFetch。
2.PHPでモデル名を検索して、見つかればモデルの生データをSend。
3.データが返ってきたら生データにURLを付与してmodel-viewerのsrcに流し込む。

というシーケンスです。

では カスタムHTML ⇒ JS ⇒ PHP の流れで見ていきましょう。
<script  type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
<model-viewer class="mviewer" name="modelname" loading="lazy" type="model/gltf-binary" exposure="0.6"></model-viewer>
document.addEventListener("DOMContentLoaded", () => {
    var mvlist = document.querySelectorAll(".mviewer");
    if(mvlist){
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if(entry.isIntersecting){
                    loadmodel(entry.target);
                    observer.unobserve(entry.target);
                }
            })
        })
        mvlist.forEach(mv => {observer.observe(mv);});
    }
});

function loadmodel(target){
    const fileName = target.getAttribute("name") + ".glb"; 
    const url = `/wp/response.php`;

    fetch(url, {
        method: "POST",
        headers: { 'Content-Type': 'model/gltf-binary' }, 
        body : fileName
    })
    .then(res => {
        if (!res.ok) throw new Error("取得失敗" + url);
        return res.blob();
    })
    .then(blob => {
        const objectURL = URL.createObjectURL(blob);
        target.src = objectURL;
    })
    .catch(err => console.error("モデル読み込みエラー: ", err + url));
}
<?php
if (class_exists( 'loadmodel' ) ) {
    error_log("double exist ");
	return;
}

$filename = file_get_contents('php://input');
if (!preg_match('/^[a-zA-Z0-9_\-]+\.glb$/', $filename)) {
    error_log("loadmodel not glb : " . $filename);
    http_response_code(400);
    exit("Invalid file name");
}

$baseDir = __DIR__ . '/wp-content/uploads/model/';
$filePath = $baseDir . $filename;

if (!$filePath || strpos($filePath, realpath($baseDir)) !== 0) {
    error_log("loadmodel access denied : " . $filePath);
    http_response_code(403);
    exit("Access denied");
}

if (!file_exists($filePath)) {
    error_log("loadmodel not found : " . $filePath);
    http_response_code(404);
    exit("File not found");
}

header('Content-Type: model/gltf-binary');
header('Content-Disposition: inline; filename="'.$filename.'"');
header('Content-Length:' . filesize($filePath));
readfile($filePath);
exit;
?>

JSでは、intersectingObserverでmodelviewerの表示を検知しFetchを実行します。
Fetchでは指定したPHP(response.php)にファイル名を投げて処理を依頼。
レスポンスが返ってきたら中身をチェックして、問題なければ中身にURLを付与してModelViewerのSrcに渡します。

なお、表示されるたびに実行されても無駄なので一回実行したらObserve対象から外しています。


PHPはかなりシンプルで、Fetchでもらったファイル拡張子のチェック・パスのチェック・レスポンスヘッダの記述・生データの取り出しという流れです。

注意点として、PHPはhttpから始まるURLは処理できません。
パスを渡すときはローカルパスで指定するようにしましょう。
PHPは全く触ったことがなかったので、ここでかなり躓いてしまいました…。
なぜかPHPの純正ログも出ないしWordpressのログも使えないしよー……チクショー…。


重要なのはcontent-typeにmodel/gltf-binaryを指定すること。
Fetchで指定するかレスポンスで指定するか、どちらかでいいと思います。たぶん。
今回は念のため両方とも書いてます。


いい感じですね!
DevToolのインスペクターにはURLも書かれていませんし、PHPからの応答も生データなのでデータの窃取もしにくくなっています。

場合によってはサーバがglb,gltfに対応してない場合があります。
その場合は.htaccessにてMIMEタイプの追加をしてみるといいかもしれません。
<IfModule mime_module>
AddType model/gltf-binary .glb
</IfModule>

この仕組みは、Content-typeを変えれば3Dデータだけでなく画像や動画にも応用できそうですね。
良ければご活用ください。