ドキュメント
/
レシピ
/

ホスト側でのテンプレート置換

ホスト側でのテンプレート置換

v6.10.0+

サーバーでレンダリングした値(例: Go html/template の `{{ .Field }}` 式)を、難読化パイプラインを壊すことなく VM 難読化された JavaScript に埋め込みます。

問題

あなたのバックエンド(Go、Rails、Django、PHP、…)は、その内容をリクエストごとに部分的にレンダリングしなければならない JavaScript ファイルを配信しています。API エンドポイント、機能フラグのリスト、初期状態のブロブ、ビルド ID、nonce などです。あなたは その JS を vmObfuscation: true で難読化しますが、同時に、難読化が完了したに、難読化された出力の中の プレースホルダをテンプレートエンジンに置換させる必要もあります。プレースホルダが VM バイトコードに取り込まれてしまうと(文字列と 識別子のデフォルトの挙動です)、テンプレートエンジンには置換するものが何もなくなります。

どのオプションを選べばよいか?

サーバーの値が…使うもの
生の JS 式(配列リテラル、オブジェクト、数値、真偽値、関数呼び出し)reservedNames + 識別子プレースホルダ
文字列で、テンプレートの区切り文字を自分で制御できるreservedStrings + テンプレートリテラルのプレースホルダ
文字列で、テンプレートエンジンが特定の区切り文字を強制する(例: Go の {{ .Field }}reservedStrings + 文字列またはテンプレートリテラルのプレースホルダ

パターン 1 - 識別子プレースホルダを使う reservedNames

最適な用途: 引用符のエスケープを一切気にせず、生の JS 式(配列、オブジェクト、数値、…)を埋め込む。

実際のコードと決して衝突しない特徴的な識別子を選び、それを直接参照し、それに一致する正規表現を reservedNames に列挙します。VM 難読化では、その識別子は予約式の配列を経由して、出力にそのまま現れます。

ソース

(function () {
    var initialState = __INITIAL_STATE__;
    bootstrap(initialState);
})();

難読化ツールのオプション

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    reservedNames: ['^__INITIAL_STATE__$']
});

難読化後

出力のどこかに、次のような予約式の配列が見つかります:

let _r = [__INITIAL_STATE__, /* …other entries… */];

トークン __INITIAL_STATE__ は識別子のリネームを生き延び、VM バイトコードに取り込まれません。

ホスト側の置換(Go の例)

tpl := template.Must(template.New("obf.js").Parse(obfuscated))
tpl.Execute(w, map[string]any{
    "InitialState": map[string]any{"user": "alice", "theme": "dark"},
})

…ここで Go テンプレートがそのトークンを生の JS 式に置き換えます:

{{ `__INITIAL_STATE__` }} → {{ .InitialState | toJSON }}

結果:

let _r = [{"user":"alice","theme":"dark"}, /* … */];

引用符もエスケープも不要です。埋め込まれた値は、JS エンジンによって通常の式として解析されます。

パターン 2 - テンプレートリテラルのプレースホルダを使う reservedStrings

最適な用途: バッククォートで囲むとたまたま有効な JS テキストになる {{ .Field }} のような区切り文字を必要とする、 Go / Jinja スタイルのテンプレートエンジン。

プレースホルダは、単一の quasi で補間のないテンプレートリテラルの中に置かれます。VM 難読化では、これは 予約式の配列を経由し、出力内の生のバッククォート形式を保持します。

ソース

(function () {
    var featureFlags = JSON.parse(`{{.Config.FeatureFlags}}`);
    applyFlags(featureFlags);
})();

難読化ツールのオプション

UI

上記のソースから {{.Config.FeatureFlags}} プレースホルダを予約するには、この正規表現を Reserved Strings フィールドに、バックスラッシュ 1 つで追加します。UI は値をそのまま保存するため、JS コードとは異なりバックスラッシュを 二重にする必要はありません:

\{\{[^}]+\}\}

バックスラッシュ 1 つで正規表現を入力した、難読化ツール UI の Reserved Strings フィールド

API

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    reservedStrings: ['\\{\\{[^}]+\\}\\}']
});

難読化後

let _r = [`{{.Config.FeatureFlags}}`, /* … */];

プレースホルダは、周囲のバッククォートを含めてバイト単位で保持されます。

ホスト側の置換

{{.Config.FeatureFlags}} を有効な JSON 文字列に置き換えます。実行時にはバッククォートの内側に収まるため、 内側の引用符のエスケープは不要です:

flagsJSON, _ := json.Marshal(config.FeatureFlags) // e.g. {"newCheckout":true,"darkMode":false}
out := strings.ReplaceAll(obfuscated, "{{.Config.FeatureFlags}}", string(flagsJSON))

実行時: JSON.parse(`{"newCheckout":true,"darkMode":false}`) - 動作します。

パターン 3 - 引用符で囲まれた文字列プレースホルダを使う reservedStrings

最適な用途: ES5 のままでなければならない(テンプレートリテラルがない)ソースコード、または周囲の API が 通常の文字列リテラルを期待するケース。

プレースホルダは、シングルまたはダブルクォートで囲まれた文字列リテラルです。VM 難読化では、それは 予約文字列の配列を経由します。この配列は JSON.stringify でシリアライズされた JS 配列として出力されます。 入力の引用符スタイルに関係なく、常にダブルクォートになります

ソース

(function () {
    var tags = JSON.parse("{{.Page.Tags}}");
    renderTags(tags);
})();

難読化ツールのオプション

UI

上記のソースから {{.Page.Tags}} プレースホルダを予約するには、この正規表現を Reserved Strings フィールドに、 バックスラッシュ 1 つで追加します。UI は値をそのまま保存するため、JS コードとは異なりバックスラッシュを二重にする必要はありません:

\{\{[^}]+\}\}

バックスラッシュ 1 つで正規表現を入力した、難読化ツール UI の Reserved Strings フィールド

API

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    reservedStrings: ['\\{\\{[^}]+\\}\\}']
});

難読化後

var _rs = ["{{.Page.Tags}}", /* … */];

ホスト側の置換

プレースホルダがダブルクォートの JS 文字列の中に収まるため、埋め込む JSON ペイロードは内側の " 文字を \" でエスケープしなければなりません:

raw, _ := json.Marshal(page.Tags) // e.g. ["news","tech","release"]
// Escape " for embedding inside a JS double-quoted string.
escaped := strings.ReplaceAll(string(raw), `"`, `\"`)
out := strings.ReplaceAll(obfuscated, "{{.Page.Tags}}", escaped)

結果の出力:

var _rs = ["[\"news\",\"tech\",\"release\"]", /* … */];

実行時: JSON.parse("[\"news\",\"tech\",\"release\"]")["news","tech","release"]

エスケープを忘れると、ブラウザは引用符の対応が取れていないと判断し、SyntaxError を投げます。パターン 2 は バッククォートを使うことで、これを完全に回避します。

1 つのプログラム内の複数のプレースホルダ

3 つのパターンはすべて組み合わせられます。選択(alternation)を含む単一の reservedStrings 正規表現で、テンプレートエンジンが 発行するあらゆるプレースホルダの形に一致させられます:

JavaScriptObfuscator.obfuscate(source, {
    vmObfuscation: true,
    reservedNames: ['^__INITIAL_STATE__$'],
    reservedStrings: ['\\{\\{[^}]+\\}\\}']
});

同じソース内で、識別子プレースホルダ(生の JS 値用)と文字列プレースホルダ(レンダリングされた JSON 用)を混在させられます。 サーバーが実際に何を注入するかに基づいて、プレースホルダごとに選んでください。

互換性に関する注意

  • 識別子と文字列の間でプレースホルダを使い回さないこと。 __TOKEN__ のような予約名と、__TOKEN__ に一致する予約文字列は、2 つの異なるコードパス(予約式の配列と予約文字列の配列)を表します。それぞれに異なるテキストの形を使ってください。例えば、識別子プレースホルダには UPPER_SNAKE の規約を、文字列プレースホルダには区切り文字で囲む形({{ ... }}%{...}<<<...>>>)を使います。そうすれば、一方の正規表現のバグがもう一方に無言で一致してしまうことを防げます。
  • reservedStrings の正規表現は生の文字列値に対して実行されます。 正規表現は、ソーステキストではなく文字列の実行時の値に対して照合されます。\{\{[^}]+\}\} は、{{.something}}(またはその他の {{...}} の形)を含む文字列に一致します。プレースホルダが余分な内容で囲まれている可能性がある場合("prefix-{{.Field}}-suffix")、正規表現は依然として一致しますが、文字列全体が保持されます。それに応じて置換を計画してください。
  • どんなテンプレートエンジンでも動作します。 例では Go の text/template 構文を使っていますが、javascript-obfuscator の統合に Go 固有のものは何もありません。難読化ツールの出力に対して文字列レベルの置換ができるものであれば、何でも動作します。Rails ERB、Django、PHP のショートタグ、CI パイプラインの sed などです。あなたのエンジンが自然に発行し、かつ実際の JS 構文と衝突しない区切り文字を選んでください。