はじめに
この間、Webサービスの追加機能デプロイ時に 「uglifyjs-webpack-plugin」 というNode.jsのライブラリを使ってJavaScriptの難読化を行なったところ不具合が発生しました。
しかし原因はライブラリでもバージョンのズレなどでもなく、「ソースコードの難読化」自体にありました。
この記事では、「JavaScriptの難読化」 に潜む落とし穴とその対策についてお話しします。
私自身考えもしなかった落とし穴でしたので、皆様もぜひ参考にしてください。
ソースコードの難読化とは
なぜ、JavaScript を難読化する必要があるのか?
こちらの記事がとても参考になりますので、分からない方はご一読ください。
ソースコードの難読化とは、処理内容を変えず、とにかく読みづらくすることです。
言っていることそのまんまな気がしますが、具体例を見ていきましょう。
function sum(nums) {
let result = 0;
nums.forEach(num => result += num);
return result;
}
このコードは誰が見ても 「数値の和を求める関数」 で、このままではWebサービスのユーザーに簡単に理解されてしまいます。
そこで一般的には、ライブラリなどを用いて以下のように難読化を行います。
function s(n) {
let r = 0;
n.forEach(m => r += m);
return r;
}
このように、変数名や関数名、クラス名を意味のない文字列にすることで、
ぱっと見で理解できないソースコードに変換することを難読化といいます。(ざっくり)
落とし穴
一見すると、難読化で不具合が発生することは無いように見えます。
しかしあります。変数名や関数名、クラス名を変えられて困ることが1つだけあります。
そうです、その名前の文字列で参照する瞬間です。
特に動的な言語では名前の文字列を使用することがあると思います。
私が実際にこの落とし穴にハマった時は、このようなコードを書いていました。
// 実際のコードとは異なります
class User {
// 略
}
if ( (typeof user == 'object') && (user.constructor.name == 'User') ) {
// 略
}
「TypeScript使えー」とか「文字列で判断するなー」とかは一旦置いておいて、
まさに先ほど言った通り クラス名を文字列を用いて比較 しています。
難読化前は、user
変数はUser
クラスのインスタンスで、
user.constructor.name
は"User"
を返します。
しかしこのコードを難読化すると下のようになります。
// クラス名がUserではない!!
class U {
// 略
}
if ( (typeof u == 'object') && (u.constructor.name == 'User') ) {
// 略
}
これが落とし穴です。
そうです、難読化後のu.constructor.name
は"U"
を返すのです。
それはもう難読化前後で挙動が違うわけですね。
なぜこのようなコードを書いたか
実は、JavaScriptにclassの概念はありません。
classを書いているのは全て見せかけで、実際は全て関数やobjectに翻訳されます。
class User {}
let user = new User();
console.log(typeof User); // => function
console.log(typeof user); // => object
そのためJavaScriptでは、そのインスタンスのクラスを判断するには以下のように書きます。
class User {}
let user = new User();
console.log(user.constructor.name); // => User
console.log(user.constructor.name === 'User'); // => true
このconstructor
の文字列比較が良くありません。
左辺はソースコードの難読化によって異なる結果を返すからです。
ではどうすればよいか
結論としては、難読化するなら文字列で比較するなということです。
原因が分かれば当たり前な答えですが、全く考えもしなかった昔の私に言いたいアドバイスでもあります。
今回の例で言えば、以下のように変えることで落とし穴を回避することができます。
class User {
// 略
}
// == 'User' を == User.name に変える
if ( (typeof user == 'object') && (user.constructor.name == User.name) ) {
// 略
}
// またはconstructor自体で比較
if ( (typeof user == 'object') && (user.constructor == User) ) {
// 略
}
// 継承元のconstructorも全て調べられる[2022/11/08追記]
if ( user instanceof User ) {
// 略
}
最後に
この記事では私が遭遇したソースコード難読化の落とし穴を紹介しました。
本来であればTypeScriptを用いたり開発ルールを設けたりで回避できるものなので、
皆様もこれを機にぜひ注意してください。