22
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

デプロイ後に発見したJavaScript難読化の落とし穴

Last updated at Posted at 2022-11-07

はじめに

この間、Webサービスの追加機能デプロイ時に uglifyjs-webpack-plugin というNode.jsのライブラリを使ってJavaScriptの難読化を行なったところ不具合が発生しました。
しかし原因はライブラリでもバージョンのズレなどでもなく、「ソースコードの難読化」自体にありました。
この記事では、「JavaScriptの難読化」 に潜む落とし穴とその対策についてお話しします。
私自身考えもしなかった落とし穴でしたので、皆様もぜひ参考にしてください。

ソースコードの難読化とは

なぜ、JavaScript を難読化する必要があるのか?
こちらの記事がとても参考になりますので、分からない方はご一読ください。

ソースコードの難読化とは、処理内容を変えず、とにかく読みづらくすることです。
言っていることそのまんまな気がしますが、具体例を見ていきましょう。

javascript
function sum(nums) {
  let result = 0;
  nums.forEach(num => result += num);
  return result;
}

このコードは誰が見ても 「数値の和を求める関数」 で、このままではWebサービスのユーザーに簡単に理解されてしまいます。
そこで一般的には、ライブラリなどを用いて以下のように難読化を行います。

javascript(難読化後)
function s(n) {
  let r = 0;
  n.forEach(m => r += m);
  return r;
}

このように、変数名や関数名、クラス名を意味のない文字列にすることで、
ぱっと見で理解できないソースコードに変換することを難読化といいます。(ざっくり)

落とし穴

一見すると、難読化で不具合が発生することは無いように見えます。
しかしあります。変数名や関数名、クラス名を変えられて困ることが1つだけあります。

そうです、その名前の文字列で参照する瞬間です。
特に動的な言語では名前の文字列を使用することがあると思います。
私が実際にこの落とし穴にハマった時は、このようなコードを書いていました。

javascript
// 実際のコードとは異なります
class User {
  // 略
}

if ( (typeof user == 'object') && (user.constructor.name == 'User') ) {
  // 略
}

「TypeScript使えー」とか「文字列で判断するなー」とかは一旦置いておいて、
まさに先ほど言った通り クラス名を文字列を用いて比較 しています。

難読化前は、user変数はUserクラスのインスタンスで、
user.constructor.name"User"を返します。

しかしこのコードを難読化すると下のようになります。

javascript(難読化後)
// クラス名がUserではない!!
class U {
  // 略
}

if ( (typeof u == 'object') && (u.constructor.name == 'User') ) {
  // 略
}

これが落とし穴です。
そうです、難読化後のu.constructor.name"U"を返すのです。
それはもう難読化前後で挙動が違うわけですね。

なぜこのようなコードを書いたか

実は、JavaScriptにclassの概念はありません。
classを書いているのは全て見せかけで、実際は全て関数やobjectに翻訳されます。

javascript
class User {}

let user = new User();

console.log(typeof User); // => function
console.log(typeof user); // => object

そのためJavaScriptでは、そのインスタンスのクラスを判断するには以下のように書きます。

javascript
class User {}

let user = new User();

console.log(user.constructor.name); // => User
console.log(user.constructor.name === 'User'); // => true

このconstructorの文字列比較が良くありません。
左辺はソースコードの難読化によって異なる結果を返すからです。

ではどうすればよいか

結論としては、難読化するなら文字列で比較するなということです。
原因が分かれば当たり前な答えですが、全く考えもしなかった昔の私に言いたいアドバイスでもあります。

今回の例で言えば、以下のように変えることで落とし穴を回避することができます。

javascript
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を用いたり開発ルールを設けたりで回避できるものなので、
皆様もこれを機にぜひ注意してください。

22
13
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?