iT邦幫忙

2024 iThome 鐵人賽

DAY 21
1

前言

JSON Web Token (JWT) 是一種廣泛使用的身分驗證機制,但如果實作不當,可能會導致嚴重的安全漏洞。這篇文章深入探討 JWT 的安全性,分析一個不安全的 JWT 實作,並討論如何改進。

JWT 的基本概念

JWT 是一種開放標準(RFC 7519),簡潔(compact) 且自包含(self-contained) ,用於在雙方之間安全地傳輸資訊作為 JSON 物件。

這些資訊可以被驗證和信任,他透過數位簽章方式進行驗證

自包含」(self-contained)

  1. 完整性:JWT 本身包含了所有必要的資訊,不需要在伺服器端儲存額外的狀態或資料。
  2. 獨立性:每個 JWT 都可以獨立驗證,不需要查詢資料庫或其他外部資源。
  3. 資訊豐富:JWT 可以包含使用者身分、權限等相關資訊,使得接收方可以直接從 token 中取得所需資料。
  4. 可攜性:JWT 可以輕易地在不同的系統間傳遞,只要系統知道如何處理 JWT。
  5. 無狀態:伺服器不需要維護 session 狀態,每次請求都帶有完整的驗證資訊。
  6. 自驗證:通過數位簽章,JWT 可以自行證明其完整性和真實性,不需額外的驗證步驟。

一個典型的 JWT 可能包含使用者 ID、使用者名稱、過期時間等資訊。當伺服器收到這個 JWT 時,它可以直接從 token 中解析出這些資訊,而不需要查詢資料庫。這就體現了 JWT 的「自包含」特性。

這種自包含的特性使得 JWT 特別適合用於分散式系統和微服務架構,因為它減少了系統間的相依性和通訊需求。

組成

JWT 主要由三個部分組成,每個部分之間用點(.)分隔:

  1. Header (標頭): 包含 token 的類型和使用的演算法。
  2. Payload (內容): 包含聲明(claims),例如使用者 ID、名稱等。
  3. Signature (簽章): 用於驗證 token 的完整性和真實性。

Header (標頭)

  • 格式:JSON 物件,然後經過 Base64Url 編碼。
  • 內容:通常包含兩個欄位:
  • typ:指定 token 的類型,通常為 "JWT"。
  • alg:指定用於生成簽章的演算法,如 "HS256"(HMAC SHA256)或 "RS256"(RSA SHA256)。
  • 範例:
{
  "alg": "HS256",
  "typ": "JWT"
}
  • 編碼後:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload (內容)

  • 格式:JSON 物件,然後經過 Base64Url 編碼。
  • 內容:包含聲明(claims)。聲明是關於實體(通常是使用者)和其他資料的陳述。常見的聲明包括:
  • iss (Issuer):發行者
  • sub (Subject):主題
  • aud (Audience):接收者
  • exp (Expiration Time):過期時間
  • nbf (Not Before):生效時間
  • iat (Issued At):發行時間
  • jti (JWT ID):JWT 的唯一識別碼
  • 也可以包含自定義聲明。
  • 範例:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
  • 編碼後:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

Signature (簽章)

  • 目的:用於驗證 token 的完整性,確保 token 在傳輸過程中沒有被竄改。
  • 生成方式:使用 header 中指定的演算法,結合 header、payload 和一個密鑰(secret)來生成。
  • 公式:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)
  • 範例(使用 "your-256-bit-secret" 作為密鑰):
    SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

完整的 JWT 就是這三個部分用點(.)連接起來:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

image

可使用 jwt.io 來查看與驗證

https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

重要說明

  • Header 和 Payload 是以明文形式存在的,只是經過 Base64Url 編碼,可以輕易解碼。因此,不應在 Payload 中放置敏感資訊。
  • Signature 是保證 JWT 完整性和真實性的關鍵。只有擁有密鑰的一方才能生成和驗證正確的簽章。
  • JWT 的安全性主要相依套件於密鑰的保密性和簽章的正確驗證。

這種結構使得 JWT 成為一種自包含的 token,接收方可以獨立驗證 token 的真實性,而不需要額外的資料庫查詢。

實作範例

程式碼

https://github.com/fei3363/ithelp_web_security_2024/commit/9ccddf754b65d4108888aed7346eff23bd03d375

實作步驟

  1. 安裝必要的套件:
    package.json 中新增 jsonwebtoken 套件:

    "dependencies": {
      // ... 其他相依套件
      "jsonwebtoken": "^8.5.1"
    }
    

    然後執行 npm install 安裝新增的套件。

  2. 建立 JWT
    新建檔案 web/controllers/authJWTController.js

    const jwt = require('jsonwebtoken');
    const db = require('../db/postgres');
    
    const Secretkey = 'mysecret'; // 不安全的密鑰
    
    const authJWTHandler = {
      // ... 在此處實作各個方法
    };
    
    module.exports = authJWTHandler;
    
  3. 實作登入功能:
    authJWTHandler 物件中新增 login 方法:

    async login(req, res) {
      const { username, password } = req.body;
      try {
        const user = await db.query('SELECT * FROM users WHERE username = $1', [username]);
        if (user.rows.length === 0) {
          return res.status(401).json({ message: '使用者不存在或密碼錯誤' });
        }
        const userObj = user.rows[0];
    
        // 驗證密碼,不安全的寫法
        const isPasswordValid = password === userObj.password;
        if (!isPasswordValid) {
          return res.status(401).json({ message: '使用者不存在或密碼錯誤' });
        }
    
        // 生成 token,不安全寫法
        const token = jwt.sign({ id: userObj.id, username: userObj.username, password: userObj.password }, Secretkey);
        res.json({ message: '登入成功', token });
      } catch (error) {
        res.status(500).json({ error: error.message });
      }
    }
    
  4. 實作 token 刷新功能:
    authJWTHandler 物件中新增 refresh 方法:

    async refresh(req, res) {
      const token = req.headers.authorization;
      if (!token) {
        return res.status(401).json({ message: '沒有提供 token' });
      }
      try {
        const decoded = jwt.verify(token, Secretkey);
        const newToken = jwt.sign({ id: decoded.id, username: decoded.username }, Secretkey);
        res.json({ message: 'Token 更新成功', token: newToken });
      } catch (error) {
        res.status(401).json({ message: 'Token 驗證失敗' });
      }
    }
    
  5. 實作驗證中間 middleware:
    authJWTHandler 物件中新增 insecureAuth 方法:

    insecureAuth(req, res, next) {
      const token = req.header('Authorization');
      if (!token) return res.status(401).json({ error: 'Access denied' });
      try {
        const verified = jwt.verify(token, Secretkey);
        req.user = verified;
        next();
      } catch (err) {
        res.status(400).json({ error: 'Invalid token' });
      }
    }
    
  6. 實作受保護的路由處理函數:
    authJWTHandler 物件中新增 protectedRoute 方法:

    protectedRoute(req, res) {
      res.json({ message: 'This is a protected route', user: req.user });
    }
    
  7. 建立路由:
    新建檔案 web/routes/authJWTRouter.js

    const express = require('express');
    const router = express.Router();
    const authJWTHandler = require('../controllers/authJWTController');
    
    router.post('/login', authJWTHandler.login);
    router.post('/refresh', authJWTHandler.refresh);
    router.get('/protected', authJWTHandler.insecureAuth, authJWTHandler.protectedRoute);
    
    module.exports = router;
    
  8. 在主應用程式中使用路由:
    web/server.js 中新增:

    const authJWTRouter = require('./routes/authJWTRouter');
    // ... 其他程式碼
    app.use('/api/authJWT', authJWTRouter);
    

這個實作過程建立了一個基本的 JWT 認證系統,但故意包含了多個安全漏洞。這樣的設計目的是為了展示常見的錯誤做法,並在後續的學習中逐步改進這些問題。在實際的正式環境中,應該從一開始就實施安全的做法,而不是先實作不安全的版本再進行改進。

不安全的 JWT 實作分析

讓我們來看一個不安全的 JWT 實作範例:

const jwt = require('jsonwebtoken');
const db = require('../db/postgres');

const Secretkey = 'mysecret'; // 不安全的密鑰

const authJWTHandler = {
    async login(req, res) {
        const { username, password } = req.body;
        try {
            const user = await db.query('SELECT * FROM users WHERE username = $1', [username]);
            if (user.rows.length === 0) {
                return res.status(401).json({ message: '使用者不存在或密碼錯誤' });
            }
            const userObj = user.rows[0];

            // 驗證密碼,不安全的寫法
            const isPasswordValid = password === userObj.password;
            if (!isPasswordValid) {
                return res.status(401).json({ message: '使用者不存在或密碼錯誤' });
            }

            // 不安全寫法,將密碼也一起放入 token 中
            const token = jwt.sign({ id: userObj.id, username: userObj.username, password: userObj.password }, Secretkey);
            res.json({ message: '登入成功', token });
        } catch (error) {
            res.status(500).json({ error: error.message });
        }
    },

    // ... 其他方法 ...
};

程式碼說明

  1. 登入功能 (login):

    • 從請求中取得使用者名稱和密碼。
    • 查詢資料庫以驗證使用者。
    • 直接比對密碼。
    • 生成包含使用者資訊(包括密碼)的 JWT。
  2. Token 刷新功能 (refresh):

    • 從請求標頭取得 token。
    • 驗證 token,但沒有檢查過期時間。
    • 生成新的 token。
  3. 驗證中間件 (insecureAuth):

    • 檢查請求標頭中是否存在 token。
    • 驗證 token 的有效性。
    • 將解碼後的使用者資訊附加到請求物件上。
  4. 受保護的路由 (protectedRoute):

    • 回傳一個包含使用者資訊的 JSON 回應。

安全問題

  1. 弱密鑰: 使用硬編碼的簡單字串作為密鑰。
  2. 密碼儲存: 密碼以明文形式儲存和比對。
  3. 敏感資訊洩漏: JWT 中包含了密碼。
  4. 缺少過期機制: Token 沒有設定過期時間。
  5. 不安全的 token 刷新: 刷新時沒有驗證原始 token 的有效性。

改進建議

  1. 使用環境變數儲存強密鑰。
  2. 使用密碼雜湊函數(如 bcrypt)來儲存和驗證密碼。
  3. 不要在 JWT 中包含敏感資訊。
  4. 為 token 設定合理的過期時間。
  5. 實作安全的 token 刷新機制,包括驗證原始 token 的有效性和過期狀態。
  6. 使用 HTTPS 來加密所有通訊。
  7. 實作 token 黑名單機制來處理登出和 token 撤銷。

實際測試

讓我們使用 curl 指令來測試這個不安全的 JWT 實作:

Lab 1: 登入並取得 token

curl -s -X POST http://nodelab.feifei.tw/api/authJWT/login -H "Content-Type: application/json" -d '{"username":"testuser","password":"password123"}'

image

結果:

{"message":"登入成功","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ0ZXN0dXNlciIsInBhc3N3b3JkIjoicGFzc3dvcmQxMjMiLCJpYXQiOjE3MjgxNDI0NTF9.8ahDgVhrLzitJmFt8JtuuPutHkDmU7m0eFhqBhliKfw"}

這個 lab 展示了如何取得 JWT。注意 token 是如何在登入成功後立即被回傳的。

Lab 2: 解碼 JWT payload

echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ0ZXN0dXNlciIsInBhc3N3b3JkIjoicGFzc3dvcmQxMjMiLCJpYXQiOjE3MjgxNDI0NTF9.8ahDgVhrLzitJmFt8JtuuPutHkDmU7m0eFhqBhliKfw" | cut -d"." -f2 | base64 -d

image

結果:

{"id":1,"username":"testuser","password":"password123","iat":1728142451}

這個 lab 展示了 JWT 的內容是如何容易被解碼的。我們可以看到,密碼被明文包含在 token 中,這是一個嚴重的安全問題。

Lab 3: 存取受保護的路由

curl http://nodelab.feifei.tw/api/authJWT/protected -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ0ZXN0dXNlciIsInBhc3N3b3JkIjoicGFzc3dvcmQxMjMiLCJpYXQiOjE3MjgxNDI0NTF9.8ahDgVhrLzitJmFt8JtuuPutHkDmU7m0eFhqBhliKfw"

image

結果:

{"message":"This is a protected route","user":{"id":1,"username":"testuser","password":"password123","iat":1728142451}}

這個 lab 展示了如何使用 JWT 存取受保護的路由。注意伺服器如何回傳完整的使用者資訊,包括密碼。

Lab4: 更新沒有驗證,可重複更新

curl -X POST http://nodelab.feifei.tw/api/authJWT/refresh -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ0ZXN0dXNlciIsInBhc3N3b3JkIjoicGFzc3dvcmQxMjMiLCJpYXQiOjE3MjgxNDI0NTF9.8ahDgVhrLzitJmFt8JtuuPutHkDmU7m0eFhqBhliKfw"

image

結果:

{"message":"Token 更新成功","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ0ZXN0dXNlciIsImlhdCI6MTcyODE0MjY0OH0.-OEQyI0swykczw64vePQKj1RmsRkuquaEIs06iBceuU"}

安全改進建議

  1. 使用強密鑰
    • 使用長度至少 32 位元組的隨機生成密鑰
    • 通過環境變數儲存密鑰,避免硬編碼
  2. 安全儲存密碼
    • 使用 bcrypt 等演算法對密碼進行雜湊處理
    • 永遠不要以明文形式儲存密碼
  3. 移除敏感資訊:
    • 不要在 JWT 中包含密碼等敏感資訊
    • 只包含必要的資訊,如使用者 ID 和權限
  4. 新增過期機制:
    • 為 JWT 新增合理的過期時間
    • 實作 token 刷新機制
  5. 實作 token 黑名單:
    • 用於處理登出和 token 撤銷
    • 可以使用 Redis 等快取系統來實作
  6. 使用 HTTPS:
    • 確保所有通訊都通過 HTTPS 進行
    • 防止中間人攻擊和資訊竊取

改進後的程式碼範例

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const db = require('../db/postgres');

const SECRET_KEY = process.env.JWT_SECRET; // 從環境變數取得密鑰
const TOKEN_EXPIRY = '1h'; // token 有效期為 1 小時

const authJWTHandler = {
    async login(req, res) {
        const { username, password } = req.body;
        try {
            const user = await db.query('SELECT * FROM users WHERE username = $1', [username]);
            if (user.rows.length === 0) {
                return res.status(401).json({ message: '使用者不存在或密碼錯誤' });
            }
            const userObj = user.rows[0];

            // 使用 bcrypt 驗證密碼
            const isPasswordValid = await bcrypt.compare(password, userObj.password);
            if (!isPasswordValid) {
                return res.status(401).json({ message: '使用者不存在或密碼錯誤' });
            }

            // 生成不包含敏感資訊的 token
            const token = jwt.sign(
                { id: userObj.id, username: userObj.username },
                SECRET_KEY,
                { expiresIn: TOKEN_EXPIRY }
            );
            res.json({ message: '登入成功', token });
        } catch (error) {
            res.status(500).json({ error: error.message });
        }
    },

    // ... 其他方法的改進 ...
};

結論

JWT 是一個強大的身分驗證工具,但必須謹慎實作以確保安全性。通過遵循最佳實作方法,如使用強密鑰、安全儲存密碼、避免在 token 中包含敏感資訊,以及實作適當的過期和撤銷機制,可以顯著提高 JWT 實作的安全性。

小試身手

  1. JWT 中的三個部分是什麼?
    A) Header,Payload,Signature
    B) Username,Password,Token
    C) Encrypt,Decrypt,Verify
    D) Login,Logout,Refresh

    答案:A

    解析:JWT(JSON Web Token)由三個部分組成:

    • Header(標頭):包含 token 類型和使用的加密演算法。
    • Payload(內容):包含聲明(claims),如使用者身份資訊。
    • Signature(簽名):用於驗證 token 的完整性和真實性。
  2. 為什麼在 JWT 中包含密碼是不安全的?
    A) 會使 token 變得太長
    B) 可能導致密碼被洩漏
    C) 會降低系統效能
    D) 會使 token 無法解碼

    答案:B

    解析:在 JWT 中包含密碼是極不安全的,因為:

    • JWT 的 payload 部分是可以被輕易解碼的(僅做 base64 編碼)。
    • 如果 token 被攔截或洩漏,攻擊者可以直接獲取使用者的密碼。
    • 這違反了最小權限原則,token 中應只包含必要的資訊。
  3. 使用環境變數儲存 JWT 密鑰的主要目的是什麼?
    A) 提高系統效能
    B) 方便更改密鑰
    C) 增加安全性,避免密鑰被硬編碼
    D) 減少程式碼量

    答案:C

    解析:使用環境變數儲存 JWT 密鑰主要是為了增加安全性:

    • 避免密鑰被直接硬編碼在程式碼中,降低洩漏風險。
    • 允許在不同環境(開發、測試、正式)使用不同的密鑰。
    • 使密鑰管理更加靈活和安全,符合安全最佳實作方法。
  4. bcrypt 用於什麼目的?
    A) 加密 JWT
    B) 生成隨機密鑰
    C) 安全地儲存和驗證密碼
    D) 加速資料庫查詢

    答案:C

    解析:bcrypt 主要用於安全地儲存和驗證密碼:

    • 它是一種單向雜湊函數,專門設計用於處理密碼。
    • bcrypt 自動處理加鹽(salt)過程,增加抗彩虹表攻擊的能力。
    • 它的計算速度較慢,這在密碼學中是一個優點,可以抵禦暴力破解攻擊。
  5. 為什麼給 JWT 新增過期時間很重要?
    A) 增加 token 的長度
    B) 提高系統效能
    C) 限制被盜用 token 的有效期
    D) 使 token 更容易解碼

    答案:C

    解析:給 JWT 新增過期時間很重要,主要是為了安全考量:

    • 限制被盜用 token 的有效期,減少潛在的損害。
    • 強制使用者定期重新認證,提高系統的整體安全性。
    • 有助於管理 token,避免長期有效的 token 累積造成的安全風險。
    • 符合安全最佳實作方法,定期刷新認證資訊。

上一篇
資安這條路:Day 20 SSRF 漏洞解析與於 docker-compose 實作其他服務
下一篇
資安這條路 Day 22: Cache Pollution 與 HTTP Parameter Pollution (HPP)
系列文
資安這條路:系統化學習網站安全與網站滲透測試30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言