《API Testing and Development with Postman》最新第二版封面
文章目錄
- 第十四章 API 安全測試
- 1 OWASP API 安全清單
- 1.1 相關背景
- 1.2 OWASP API 安全清單
- 1.3 認證與授權
- 1.4 破防的對象級授權(Broken object-level authorization)
- 1.5 破防的屬性級授權(Broken property-level authorization)
- 1.6 不受控制的資源消耗(Unrestricted resource consumption)
- 1.7 不受限制地訪問業務流(Unrestricted access to business workflows)
- 1.8 不安全地使用 API 接口(Unsafe consumption of APIs)
- 2 模糊測試(Fuzzy)
- 2.1 相關概念
- 2.2 實戰:在 Postman 中執行模糊測試
- 2.3 繞開 Collection Runner 的測試方案
- 3 利用 Postman 內置隨機變量實現模糊測試
- 4 小結
寫在前面
本章為全書的倒數第二章,作者簡要介紹了 API 安全測試的相關概念,并對 OWASP API 安全清單和模糊測試(Fuzzy Testing)進行了重點講解;后半部分作者利用數據驅動測試演示了模糊測試在 Postman 中的具體應用,只可惜在實現方案和敘述的條理性上較為敷衍,導致我在實測過程中又踩了不少坑。特此梳理出來,既是對自己創新方案的復盤,也能讓更多后來者少走彎路。
第十四章 API 安全測試
本章概要
OWASP
API 安全清單- 用
Postman
進行模糊測試(Fuzz testing
)的方法
1 OWASP API 安全清單
1.1 相關背景
- OWASP(Open Web Application Security Project)是一個非營利組織,專注于提高軟件安全性;
- 提供免費、開放的資源,如工具、文檔、論壇等,幫助開發者和安全人員構建安全的應用程序;
- 知名項目包括 OWASP Top 10(十大 Web 應用安全風險)和 API Security Top 10(API安全十大風險);
- 相關資源:
- 網站門戶:https://owasp.org/;
- 2023 最新版 API 安全清單:https://owasp.org/www-project-api-security/;
1.2 OWASP API 安全清單
- API Security Top 10 是 OWASP 針對 API 安全的核心文檔,羅列了 API 面臨的十大安全風險。
- 適用于開發者、安全工程師和架構師,幫助識別和緩解API安全威脅。
1.3 認證與授權
黑客最先嘗試的攻擊方式是身份驗證,即通過用戶名密碼攻擊。
最簡單的方式是暴力破解(brute-force attack),因此 API 接口應采取在一定時間段內 限制登錄次數 的防護措施。
具體實現:以 GitPod
演示項目為例,可利用 Postman
內置的隨機數據變量高頻多次調用登錄接口(多次手動點擊或 Collection Runner
設置迭代次數):
測試腳本:
for (let i = 0, len = 50; i < len; i++) {pm.sendRequest({url: pm.variables.replaceIn('{{base_url}}/token'),method: 'POST',header: { 'Content-Type': 'multipart/form-data'},body: {mode: "formdata",formdata: [{ key: 'username', value: pm.variables.replaceIn('{{$randomUserName}}') },{ key: 'password', value: pm.variables.replaceIn('{{$randomPassword}}') }]}},function (err, resp) {if (err) {console.error(JSON.parse(err));return;}pm.test(`${i + 1}: Response JSON have detail attribute`, function () {pm.expect(resp.json()).to.eql({ "detail": "Incorrect user name or password" });});})
}
實測結果(鑒權接口疑似不具備限制登錄次數功能):
【圖 14.1 構造不同的用戶名和密碼,多次高頻調用登錄接口,模擬暴力破解過程】
1.4 破防的對象級授權(Broken object-level authorization)
這是 2023 年十大安全清單排名第一的高風險議題:
- 問題描述:未正確驗證用戶對對象的訪問權限,導致越權訪問。
- 應對措施:實施嚴格的權限驗證,確保用戶只能訪問授權資源。
- 示例:用登錄普通用戶(
user1/12345
)的登錄令牌去訪問管理員帳號,看看示例項目是否會響應 403 錯誤。
實測結果:
【圖 14.2 用 user1 的登錄令牌訪問管理員 admin 的帳號信息,后臺報 403 錯誤(符合預期)】
1.5 破防的屬性級授權(Broken property-level authorization)
示例:用 user1/12345
登錄,先用 "user1"
為創建人(即 create_by
字段)添加一個正常的待辦項,然后看看是否可以通過 PUT
請求篡改該創建人信息(例如改為 "user2"
)。
測試腳本:
// PUT request's Post-response
pm.collectionVariables.set('reqPut', pm.request);// Pre-request
pm.sendRequest(pm.collectionVariables.get('reqPut'),function(err, resp) {if(err) {console.error('Updated failure:', err);return;}pm.test('PUT req: status code should be 200', () => {console.log(resp)pm.expect(resp.code).to.eq(200);});}
)// Post-response
pm.test('The creator should not be updated (user1) after running PUT req',() => {const [{ created_by: creator }] = pm.response.json();pm.expect(creator).to.eql('user1')});
實測結果:
【圖 14.3 實測屬性級授權漏洞(篡改失敗,符合預期)】
但實測發現,在創建待辦項時人為設置創建人為 非當前登錄用戶(如 "user2"
),最后仍然創建成功了,說明該接口還是有問題的:
【圖 14.4 創建任務時篡改創建人信息,最終任務創建成功,說明該接口仍然支持篡改信息】
1.6 不受控制的資源消耗(Unrestricted resource consumption)
方式一:通過耗盡所有系統資源來損害系統,導致系統癱瘓(denial-of-service attacks,拒絕服務攻擊);
方式二:由于未對接口調用次數進行限制,大量請求將拖慢響應速度;如果接口調用了第三方付費資源,還會產生大量費用。
1.7 不受限制地訪問業務流(Unrestricted access to business workflows)
攻擊者可能會利用抓包、監控請求數據等方式獲取系統內部通信 API,從而獲悉內部工作流結構,給系統帶來嚴重威脅(例如惡意逃票等)。
應對措施:仔細考慮暴露了哪些接口;嚴格控制接口訪問權限。
1.8 不安全地使用 API 接口(Unsafe consumption of APIs)
典型案例:引用了被黑客攻擊的第三方 API。
這類測試較為棘手,可能需要搭建一個模擬服務器,讓從第三方 API 響應的數據在該模擬器上先行緩沖,或者模擬危險數據進行測試,看看本地后臺是否能夠鑒別該類數據。
2 模糊測試(Fuzzy)
2.1 相關概念
定義:模糊測試(Fuzzy) 是一種通過向程序提供無效、隨機或意外的輸入來發現漏洞和錯誤的測試技術。
主要特征:
- 非通用測試技術
- 有助于發現未考慮到、或未測試到的問題
具體的運行方式:
- 手動執行:通過在 UI 中輸入偽隨機數據來完成(極少);
- 編程執行:以程序的形式運行(絕大多數)。工具如
PeachFuzzer
或自定義輸入集。
輸入的生成:
- 生成大量隨機或半隨機數據,可能包含格式錯誤或危險字符(如轉義字符、引號等)。
- 輸入通常是隨機的,但有一些邊界限制(例如,字節 vs ASCII/Unicode 字符串)。
- 輸入可以偏向已知的“危險”字符(例如轉義字符、引號等)。
輸入內容的具體方式:
- 通過命令行參數注入
- 通過文件輸入
- 通過網絡協議
- 通過 API 輸入(特別適合模糊測試,易于用程序控制)
模糊查詢的應用場景:
- 安全測試場景:揭示未能預見的攻擊方向,發現錯誤處理機制中的薄弱環節等;
- API 測試場景:具有易于訪問且數量巨大的測試輸入,尤其適合 API 測試。
2.2 實戰:在 Postman 中執行模糊測試
以 GitHub
開源項目 Big List of Naughty Strings 的 JSON
文件為數據源,利用 Collection Runner
對 GitPod
演示項目 ToDo List App
進行基于數據驅動的模糊測試,看看 POST /tasks
接口在大量隨機輸入下的響應情況。
首先從開源項目下載原始數據文件 blns.base64.json
。該原始數據為 Base64
編碼的字符串數組:
["", "dW5kZWZpbmVk", "dW5kZWY=", "bnVsbA==", ...
]
使用前需要先處理成如下格式(可使用 vim
宏的批量操作完成):
[{"naughtyString":""}, {"naughtyString":"dW5kZWZpbmVk"}, {"naughtyString":"dW5kZWY="}, {"naughtyString":"bnVsbA=="}, ...
]
然后創建測試集合 FuzzyTest
以及 POST
請求 Create a task
,其 URL
設置為 {{base_url}}/tasks
,其中 base_url
為集合變量,其值為 GitPod
演示項目的基礎 URL
(啟動鏈接:https://gitpod.io/new/#https://github.com/djwester/todo-list-testing)。
接著,在測試請求的請求體中輸入如下內容(這里有個大坑,后面會講):
{"description": "{{naughtyString}}","status": "Draft"
}
上述代碼中的 naughtyString
為數據驅動測試啟動后、經 Pre-request
腳本處理得到的動態變量:
const atob = require('atob');
const encoded_string = pm.iterationData.get("naughtyString");
pm.collectionVariables.set('naughtyString', atob(encoded_string));
對應的 Post-response
響應后腳本如下:
pm.test("Status code is 201", function() {pm.response.to.have.status(201);
});
一切準備就緒后,啟動 Collection Runner
,加載 JSON
映射文件,執行模糊測試:
【圖 14.5 利用 Collection Runner,發起基于JSON 文件數據驅動的模糊測試】
由于是分別讀取每行數據并調用創建接口,整個過程需要多等些時間,最終得到結果:
【圖 14.6 Collection Runner 運行結束后的實測截圖】
由于作者演示時用的是本地部署的 ToDo List App
,全套數據測完只用了 1'14"
,和我實測時的 23'56"
真是天壤之別(沒辦法,Python 基礎有限,幾次嘗試本地部署都失敗了)。但奇怪的是,除了 10 個請求因為網絡原因發送失敗,其余 666 個都成功了,并沒有報錯。
可高興不到一分鐘,我就知道出問題了。作者并沒有交代要用 user2
登錄,而我新增任務前壓根兒就沒登錄,導致后續的數據清空全部失敗了:
【圖 14.7 由于新增任務時沒有登錄,導致后續的批量刪除全部失敗(神坑)】
沒辦法,只能重置項目,重新來過……
先 Ctrl + C 中斷 GitPod
項目,運行重置數據命令 poetry run python remove_tables.py
,然后執行 make run-dev
重新啟動。
這次先用 user2/12345
換取登錄令牌,再到新增接口中完成鑒權:
【圖 14.8 用 user2 的登錄令牌完成新增接口的鑒權設置】
然后只選取新增接口,再次上傳 JSON
數據集運行 Collection Runner
:
【圖 14.9 重新運行 Collection Runner 的配置界面截圖】
也不知道是不是這份死磕精神感動了馬克思,第二次運行居然全部成功了:
【圖 14.10 第二次運行結果截圖(676 條數據全部新增成功)】
接著在瀏覽器查看項目首頁,也沒有書中說的 alert
注入問題(當真歐皇附體?):
借著這波好運,趕緊再跑一遍數據批量清空。在 GET {{base_url}}/task
接口的響應后腳本中輸入以下內容(直接用 JS 腳本批量刪除,書中方法太過陳舊,還得再消耗一次 Collection Runner
免費額度,不知道作者怎么想的):
const tasks = pm.response.json();
const task_ids = tasks.map(t => t.id);const base_url = pm.collectionVariables.get('base_url');
const auth = {type: 'bearer',bearer: [{key: 'token',value: pm.collectionVariables.replaceIn('{{token}}'),type: 'string'}]
};
const getCallback = task_id => (err, response) => {if(err) {console.error(err);return;}pm.test(`Deleting id(${task_id}): Status should be OK`, function() {pm.expect(response.status).to.eql('OK');});
};for(const id of task_ids) {// Delete the task by idpm.sendRequest({url: `${base_url}/tasks/${id}`,method: 'DELETE',auth}, getCallback(id));
}
結果還是報錯:
仔細一想,真相大白了:新增接口必須在請求體中手動指定 created_by
字段,否則 一律按匿名處理(就當是作者故意挖的坑吧)。
只有再重來一遍了:
再次執行批量刪除,只有一次是后臺原因刪除失敗,五次請求未發送,其余全部刪除成功,終于熬出頭了:
2.3 繞開 Collection Runner 的測試方案
其實只要具備 JavaScript
基礎,完全可以跳過 Collection Runner
的限制,在 Pre-request
/ Post-response
腳本中實現批量新增和刪除。
批量新增的純 JS 腳本實現:
-
將原始
JSON
數據集不經任何處理直接放到Postman
的私有模塊中,例如my-blns
:const data = ["", "dW5kZWZpbmVk", "dW5kZWY=", "bnVsbA==", "TlVMTA==", "KG51bGwp", // ..."e3sgIiIuX19jbGFzc19fLl9fbXJvX19bMl0uX19zdWJjbGFzc2VzX18oKVs0MF0oIi9ldGMvcGFz","c3dkIikucmVhZCgpIH19" ]; module.exports = {data };
-
新建請求
GET {{base_url}}/tasks
,用于在批量新增后查詢總的待辦項列表:- 在
Pre-request
中輸入以下腳本:
const atob = require('atob'); const { data } = pm.require('your/package/path/to/my-blns'); // console.log(data.length);const postRequest = description => ({url: pm.collectionVariables.replaceIn('{{base_url}}/tasks'),method: 'POST',header: { 'Content-Type': 'application/json' },body: {mode: 'raw',raw: JSON.stringify({description,status: "Draft",created_by: "user2"})} });data.map(d => atob(d)).map((description, i) => pm.sendRequest(postRequest(description), (err, resp) => {if(err) {console.error(err);return;}pm.test(`Create task_${i+1} completed: the status code should be 201`,() => pm.expect(resp.code).to.eql(201));}));
-
在
Post-response
輸入以下腳本:pm.test('Task list length should be greater than 0', function () {pm.expect(pm.response.json()).to.be.an('array').and.to.have.lengthOf.at.least(1, "Task list length should be greater than 0"); });
- 在
-
至于批量刪除,剛才實測過程中已經看過腳本了,這里只說明一下實現邏輯。利用列表查詢接口
GET {{base_url}}/tasks
獲取到任務列表后,批量提取任務列表的id
;然后分別調用DELETE
接口POST {{base_url}}/tasks/:id
完成刪除(注意:刪除待辦項時,別忘了在請求中帶上鑒權配置對象auth
)。
這樣就可以在 Postman
中隨意批量新增和刪除待辦任務了,完全不受 Collection Runner
的制約。
3 利用 Postman 內置隨機變量實現模糊測試
其實就是將上傳的 JSON
文件內容用 Postman
內置的各種隨機變量實現同樣的模糊測試效果,例如將新增接口請求體中的 JSON
改為:
{"description": "{{$randomLoremSentence}}","status": "Draft","created_by": "user2"
}
或者借助測試腳本,引入 lodash
實現更多模糊測試效果:
const _ = require('lodash');
// _.sample(collection): Gets a random element from collection.
const description = _.sample(["{{$randomAbbreviation}}", "{{$randomAdjective}}"]);
const jsonBody = {description,"status", "Draft","created_by": "user2"
}
4 小結
本章內容整體感覺較敷衍,上半部分介紹相關概念幾乎全是蜻蜓點水式的描述,后半段實戰環節的條理性又明顯不如前面的章節,漏掉很多關鍵細節,且在方案選擇上過分依賴 Collection Runner
,致使我在實測過程中浪費了不少免費額度。不過依次填完這些坑后,我也有機會用純 JavaScript
腳本的方式實現待辦任務的批量創建與刪除,順便學習了用 pm.sendRequest()
發送 POST
請求和 DELETE
請求的寫法,也算是因禍得福了吧。
后記
最后再跟大家分享個操作技巧,遇到不會寫的Postman
腳本,可以利用其自帶的 AI 機器人Postbot
直接給答案。本章實測中幾種pm.sendRequest()
的寫法就是這么來的,比查官方文檔快多了。大家一定要學會使用 AI 工具,千萬不要故步自封,成為 AI 時代的 “新文盲”。