從零開始打造一個會自我防禦的API實驗室
最近花了不少時間研究 API 安全,倒不是單純看那些漏洞賞金的經驗分享或理論。其實一開始就不只是想翻翻幾份 Swagger 文件、丟幾個 Burp 請求而已。那種只碰皮毛感覺怪空虛的,就乾脆動手做。一邊蓋,一邊拆。這次弄了一個祕密管理 API,整個流程算是蠻完整:JWT 驗證啦、加密存放祕密啦,還特地塞進去一個模擬 SSRF 的小洞給自己玩。本來只打算隨便試試,結果後來連資料庫遷移跟各種認證流程都摸了遍,全都在 Kali Linux 本機搞定。
這些東西拼起來,看起來就像是在防禦自己的 API,但說真的,比較想摸清楚到底設計失誤會怎麼發生。然後,再思考怎麼補救。如果只是又寫一組 CRUD,那就沒什麼意思,所以才會一直把重點擺在安全基礎跟能被利用的地方。
其實開工前有列過一些想學什麼、要做哪些項目,不過到最後還是東挖一點、西補一點。有些環節拖了好幾天,有些細節好像記得又忘,不太確定全部照原本規劃走,但至少每一步都有帶著「這裡是不是可以被攻破」的心情去測。
反正就是從頭到尾親力親為。也許成果沒有多厲害,可是在理解那些微妙的不安全設計怎麼冒出來這件事上,好像多少有點收穫吧。
這些東西拼起來,看起來就像是在防禦自己的 API,但說真的,比較想摸清楚到底設計失誤會怎麼發生。然後,再思考怎麼補救。如果只是又寫一組 CRUD,那就沒什麼意思,所以才會一直把重點擺在安全基礎跟能被利用的地方。
其實開工前有列過一些想學什麼、要做哪些項目,不過到最後還是東挖一點、西補一點。有些環節拖了好幾天,有些細節好像記得又忘,不太確定全部照原本規劃走,但至少每一步都有帶著「這裡是不是可以被攻破」的心情去測。
反正就是從頭到尾親力親為。也許成果沒有多厲害,可是在理解那些微妙的不安全設計怎麼冒出來這件事上,好像多少有點收穫吧。
為什麼選擇JWT和加密儲存來守護你的秘密
一開始想做的事情其實有點多,說是要讓整個 API 變得安全,像 JWT 驗證啊、密碼要加鹽雜湊、秘密資料不能亂放——這些原則都試著搬進來。還有 AES 那種對稱式加密,也沒少用上。權限控管的部分嘛,就是別人家東西不要動到,這應該不難理解。API 怎麼壞掉?尤其是認證跟輸入驗證那邊卡住的可能性最大,就特別去留意。順便測試 SSRF 類型的小洞,看內部請求如果被人鑽了會怎樣,大概就是這樣一個基調。工具方面,不外乎 curl、Postman 來回折騰,主機全都在自己電腦跑起來,也比較好收拾爛攤子。整個流程記得斷斷續續做筆記,希望之後查漏補缺時能省點麻煩。
當初把專案目錄拉出來,名字就很直接地丟在桌面上:`~/Desktop/secrets-ssrf-lab`。然後就是 npm 的初始化囉,其實很多人都是先開環境再說,但也有人喜歡先寫點 README 或亂備註幾句。
為了讓 Express 跑起來比較順手,裝了一堆大家常見的套件:express 不用說;helmet 裡頭多加幾道安全防線;cors 有時候方便前端測試(雖然早期根本沒前端);dotenv 用於環境參數切換,有時候小改設定不用一直手動重啟服務器;資料庫選 PostgreSQL 搭配 sequelize 和 pg,那感覺比用 SQLite 靈活些。如果要寫 TypeScript 的話,其它什麼 ts-node、typescript 也都一併裝進去了。
TypeScript 本身配置倒沒太糾結,就是 src 當主目錄、dist 負責輸出編譯結果,型別嚴格模式開下去,才不容易踩雷。有朋友建議 express request 上擴充 req.user,那只好另外搞一份 types/express.d.ts 設定一下。不過不是每個人第一次就想到這裡,有的人可能會等到型別報錯才補救。
.env 文件很快就丟上去了,有些敏感資訊還是藏外面比較保險。例如埠號就是四百上下跳動而已;資料庫連線字串用的也是常規格式;JWT 密鑰以及 AES 加密 key 都提前生出來(但到底哪天會忘記 rotate 就難講);SSRF 測試開關參數其實最早沒設,是後來碰到需求才順手補齊。有了這套配置,要分開測試或臨時換伺服器就輕鬆多了。
至於 PostgreSQL 資料庫,一開始新建大致照教科書流程走,不外乎新 database、新 user,再給對方授權。不過遇上一些語系或權限怪問題,好像 schema 預設限制太多,用戶無法直接操作表格,就又跑去修正 GRANT ALL PRIVILEGES 給 public schema 和 database 本身。有時候 sequelize 同步表結構老是失敗,多半就是這裡卡住。如果沒有釐清這些小細節,很容易 debug 半天兜不攏原因。總算資料庫層級打通以後,其它東西才能慢慢往上堆疊。
當初把專案目錄拉出來,名字就很直接地丟在桌面上:`~/Desktop/secrets-ssrf-lab`。然後就是 npm 的初始化囉,其實很多人都是先開環境再說,但也有人喜歡先寫點 README 或亂備註幾句。
為了讓 Express 跑起來比較順手,裝了一堆大家常見的套件:express 不用說;helmet 裡頭多加幾道安全防線;cors 有時候方便前端測試(雖然早期根本沒前端);dotenv 用於環境參數切換,有時候小改設定不用一直手動重啟服務器;資料庫選 PostgreSQL 搭配 sequelize 和 pg,那感覺比用 SQLite 靈活些。如果要寫 TypeScript 的話,其它什麼 ts-node、typescript 也都一併裝進去了。
TypeScript 本身配置倒沒太糾結,就是 src 當主目錄、dist 負責輸出編譯結果,型別嚴格模式開下去,才不容易踩雷。有朋友建議 express request 上擴充 req.user,那只好另外搞一份 types/express.d.ts 設定一下。不過不是每個人第一次就想到這裡,有的人可能會等到型別報錯才補救。
.env 文件很快就丟上去了,有些敏感資訊還是藏外面比較保險。例如埠號就是四百上下跳動而已;資料庫連線字串用的也是常規格式;JWT 密鑰以及 AES 加密 key 都提前生出來(但到底哪天會忘記 rotate 就難講);SSRF 測試開關參數其實最早沒設,是後來碰到需求才順手補齊。有了這套配置,要分開測試或臨時換伺服器就輕鬆多了。
至於 PostgreSQL 資料庫,一開始新建大致照教科書流程走,不外乎新 database、新 user,再給對方授權。不過遇上一些語系或權限怪問題,好像 schema 預設限制太多,用戶無法直接操作表格,就又跑去修正 GRANT ALL PRIVILEGES 給 public schema 和 database 本身。有時候 sequelize 同步表結構老是失敗,多半就是這裡卡住。如果沒有釐清這些小細節,很容易 debug 半天兜不攏原因。總算資料庫層級打通以後,其它東西才能慢慢往上堆疊。
Comparison Table:
結論 | 摘要 |
---|---|
明確的錯誤處理 | 401 錯誤碼有助於安全與除錯,避免問題來源難以查找。 |
Vault API 功能 | 設計了加密秘密資料的倉庫,包括新增、查看、更新和刪除功能,並強調嚴格的權限控管與加密機制。 |
JWT 驗證的重要性 | JWT 身份驗證需妥善管理過期時間及簽章核對,以防資安風險。 |
SSRF 漏洞風險 | 伺服器端請求偽造可能導致敏感內部資訊暴露,需特別注意用戶輸入的網址參數。 |
安全最佳實踐 | 強調在資料寫入前進行加密,對每筆資料進行細緻的存取管控,以及使用 TypeScript 幫助提前檢測 bug。 |

當TypeScript遇上Express:我的安全架構起手式
伺服器這回事,眼看準備差不多能跑起來了。不過說真的,環境弄好只是個開始。輪到第二階段──搞一套還算安全的身份驗證,才是真正進入主題。用 Node.js 和 PostgreSQL 配 TypeScript 這組合,其實很多人都試過,但每回要做登入註冊就會想:到底怎樣才算「安全」?光讓使用者能進得去系統,遠遠不夠。這裡頭有些學問,不是隨便寫兩個 API 就行。
當時手邊專案目錄大概長這樣,有點像坊間一些範本,但細節略有不同:
/src 資料夾底下分幾條線,有 models(放 User 跟 Secret 那些資料表模型),接著 routes 裡頭塞了 auth 登入註冊、vault 跟 ssrf(後面再說),middleware 是專管 JWT 驗證那種東西,再加上一堆 utils 幫忙處理加解密、簽發 token 的雜事。types 裡面丟了一份 express.d.ts,只為型別推導順手而已。剩下什麼 index.ts、.env、tsconfig.json 那些設定檔,也沒啥特別,就是按部就班拆開放。
其實整理目錄結構也沒誰規定全世界都得照搬某種形式,但如果一開始亂七八糟,後面功能多起來很容易打結。有時候覺得前期花時間排一下路徑,反倒能省不少維護麻煩——尤其等到功能增長到七八倍的時候,更明顯感受到差異。
不過說回密碼處理這塊。有在碰 API 的,大概都聽過只靠明文存密碼可危險得很,所以最基本會想到 hash 一下。我記得那陣子選 bcrypt 算是常見作法。一個 hashPassword(password) 函數拿原始密碼轉成雜湊值,而 verifyPassword(inputPassword, storedHash) 則比較用戶輸入和資料庫存的 hash,看起來只是簡單對比,實際上還藏著點防側信道攻擊的小技巧。反正重點就是,不直接暴露任何敏感資訊,也不給攻擊者輕易猜出來——大致流程就是如此啦。
當時手邊專案目錄大概長這樣,有點像坊間一些範本,但細節略有不同:
/src 資料夾底下分幾條線,有 models(放 User 跟 Secret 那些資料表模型),接著 routes 裡頭塞了 auth 登入註冊、vault 跟 ssrf(後面再說),middleware 是專管 JWT 驗證那種東西,再加上一堆 utils 幫忙處理加解密、簽發 token 的雜事。types 裡面丟了一份 express.d.ts,只為型別推導順手而已。剩下什麼 index.ts、.env、tsconfig.json 那些設定檔,也沒啥特別,就是按部就班拆開放。
其實整理目錄結構也沒誰規定全世界都得照搬某種形式,但如果一開始亂七八糟,後面功能多起來很容易打結。有時候覺得前期花時間排一下路徑,反倒能省不少維護麻煩——尤其等到功能增長到七八倍的時候,更明顯感受到差異。
不過說回密碼處理這塊。有在碰 API 的,大概都聽過只靠明文存密碼可危險得很,所以最基本會想到 hash 一下。我記得那陣子選 bcrypt 算是常見作法。一個 hashPassword(password) 函數拿原始密碼轉成雜湊值,而 verifyPassword(inputPassword, storedHash) 則比較用戶輸入和資料庫存的 hash,看起來只是簡單對比,實際上還藏著點防側信道攻擊的小技巧。反正重點就是,不直接暴露任何敏感資訊,也不給攻擊者輕易猜出來——大致流程就是如此啦。
那些年我們踩過的PostgreSQL權限坑
在 utils 資料夾底下有個 jwt.ts,像是把比較敏感的細節都丟到那邊,這樣路由什麼的就不會太雜。像密碼啊,他們也沒打算明碼存放,也沒人在那邊自己寫什麼字串比對。bcrypt 的參數嘛,好像就是用預設值,畢竟只是練習時期的東西。
JWT 產生部分,有兩個函式:generateAccessToken 跟 generateRefreshToken,都差不多意思,就是拿 userId 去簽一下名。簽名用的密鑰,是從 .env 裡頭撈出來的亂數,每一個都設定了過期時間,短的是 access token,長一點則是 refresh token。token 本身大致只塞了一些很基本資訊,例如 sub 就是代表使用者編號,大致上就醬。
為什麼偏要 JWT?有說法是它不用特地開 session,可以讓伺服器橫向擴展比較輕鬆。不少人覺得,如果密鑰不要亂搞,其實被竄改機率也頗低。至於角色驗證、權限檢查那些,用這方式好像速度會快一些。
auth 路由就拆成 routes/auth.ts,一共四條:
- /auth/register(POST):大概就是 email 跟 password 進來,把密碼哈希後丟資料庫。
- /auth/login(POST):檢查帳密,有過就回傳 access 跟 refresh token。
- /auth/refresh(POST):給 refresh token 後再發新的 access 跟 refresh。
- /auth/logout(POST):其實目前還只是個殼,未來才考慮怎麼讓 token 作廢。
中間件 requireAuth 在 middleware/jwt.ts,那作用嘛,就是看 HTTP 頭裡 Authorization 有沒有帶 Bearer 開頭的東西,再驗證 JWT。如果通過,就把 req.user 塞進去(內容只有 user id);失敗或沒帶,就直接回 401。
TypeScript 原本是不知道 req.user 是啥,所以 types/express.d.ts 裡面加了一段宣告,把 Express.Request 多補上一個 user 屬性型別,大致類似下面那種:
import { Request } from 'express';
declare global {
namespace Express {
interface Request {
user?: number;
}
}
}
然後 tsconfig.json 裡 typeRoots 要多指定一下,不然型別還是不認識:
"typeRoots": ["./types", "./node_modules/@types"]
測試流程沒太複雜,就是手動用 curl 打看看請求和回應長怎樣。例如註冊新用戶時——
curl -X POST http://localhost:4000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]", "password":"mypassword"}'
想要看到的大概就是 {"message": "User registered", "userId": 一個數字} 類似這種吧。有空也去資料庫瞄了一眼,看起來哈希後的密碼是真的存進去了。
登入流程也是差不多意思——
curl -X POST http://localhost:4000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]", "password":"mypassword"}'
收到回應時,只要有拿到 accessToken 跟 refreshToken 大致上算順利。有沒有其他小細節?偶爾可能還真漏掉,但大方向大約如此。
JWT 產生部分,有兩個函式:generateAccessToken 跟 generateRefreshToken,都差不多意思,就是拿 userId 去簽一下名。簽名用的密鑰,是從 .env 裡頭撈出來的亂數,每一個都設定了過期時間,短的是 access token,長一點則是 refresh token。token 本身大致只塞了一些很基本資訊,例如 sub 就是代表使用者編號,大致上就醬。
為什麼偏要 JWT?有說法是它不用特地開 session,可以讓伺服器橫向擴展比較輕鬆。不少人覺得,如果密鑰不要亂搞,其實被竄改機率也頗低。至於角色驗證、權限檢查那些,用這方式好像速度會快一些。
auth 路由就拆成 routes/auth.ts,一共四條:
- /auth/register(POST):大概就是 email 跟 password 進來,把密碼哈希後丟資料庫。
- /auth/login(POST):檢查帳密,有過就回傳 access 跟 refresh token。
- /auth/refresh(POST):給 refresh token 後再發新的 access 跟 refresh。
- /auth/logout(POST):其實目前還只是個殼,未來才考慮怎麼讓 token 作廢。
中間件 requireAuth 在 middleware/jwt.ts,那作用嘛,就是看 HTTP 頭裡 Authorization 有沒有帶 Bearer 開頭的東西,再驗證 JWT。如果通過,就把 req.user 塞進去(內容只有 user id);失敗或沒帶,就直接回 401。
TypeScript 原本是不知道 req.user 是啥,所以 types/express.d.ts 裡面加了一段宣告,把 Express.Request 多補上一個 user 屬性型別,大致類似下面那種:
import { Request } from 'express';
declare global {
namespace Express {
interface Request {
user?: number;
}
}
}
然後 tsconfig.json 裡 typeRoots 要多指定一下,不然型別還是不認識:
"typeRoots": ["./types", "./node_modules/@types"]
測試流程沒太複雜,就是手動用 curl 打看看請求和回應長怎樣。例如註冊新用戶時——
curl -X POST http://localhost:4000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]", "password":"mypassword"}'
想要看到的大概就是 {"message": "User registered", "userId": 一個數字} 類似這種吧。有空也去資料庫瞄了一眼,看起來哈希後的密碼是真的存進去了。
登入流程也是差不多意思——
curl -X POST http://localhost:4000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]", "password":"mypassword"}'
收到回應時,只要有拿到 accessToken 跟 refreshToken 大致上算順利。有沒有其他小細節?偶爾可能還真漏掉,但大方向大約如此。

密碼學實戰:用bcrypt和AES-256-GCM武裝你的數據
有時候登入順利,可能會讓人誤以為整個流程挺單純,不過實際上這裡頭藏了不少細節。像是在本地端用某種工具去測試刷新令牌,大致的做法或許就是送出一段POST請求,內容帶著那組看起來不起眼但又不能丟失的refresh token。收到回應後,通常預期是系統會給一組全新的access token,再附上一份更新版的refresh token。這個過程不需要再一次輸入帳密,也算便利,但偶爾也會擔心是不是哪天忘了token到底放哪兒。
另外測試遇到比較棘手的狀況,大概就是直接嘗試進入那些本該受保護的路徑,可是卻沒有送出任何認證資訊。這時候,大多數情況下,伺服器好像都會跳出一句話:「Token 好像怪怪的或是根本沒傳」。大部分開發者應該對這種訊息並不陌生。
說到這些驗證流程,總覺得安全性常常卡在一些乍看無關緊要的小地方——像什麼密碼要用雜湊存、token壽命不能拉太長、驗證機制還得一直修正。一開始就把程式結構梳理清楚,好像真的省下不少後續麻煩;畢竟如果前面亂七八糟,到後面debug只會越來越混亂。而且直接拿curl之類的指令測每條流程,比起靠前端操作,更容易察覺細節到底有沒有漏掉。不過好像也不是每次都能百分百重現所有情境,有些小問題還真得多花點時間才找得到原因。
JWT令牌雙胞胎:accessToken與refreshToken的攻防戰
在開發 API 的過程中,像 401 這種明確的錯誤處理方式,對於安全和除錯來說,其實還挺有幫助的。不是每個人一開始就會注意到,但少了這些判斷點,有時真的很難查出問題來源。
進入第三階段,大致就是把重心轉到 Vault API,也就是那種加密秘密資料的倉庫。這塊功能主要是給已經通過驗證的人用來存放自己的加密筆記、密碼或其他機密資訊。不過,不只是單純存東西而已,還得考慮很多細節——包含嚴格控管誰可以動什麼資料、然後儲存的內容一定要經過妥善加密。前面搞好的 JWT 驗證、Sequelize 還有 TypeScript 設定,到這裡才算真正派上用場。
Vault 系統其實規劃了不少功能,大概有五六項:像是「新增秘密」(POST /vault),也能「查看所有秘密」(GET /vault)、或者只看其中某一條(GET /vault/:id)。如果哪天想改內容,那就走 PUT 方法(/vault/:id);不需要了,刪掉也是幾乎一樣的路徑,只是換成 DELETE。
每筆記錄都有它自己的限制。例如保存前,都會用 AES-256-GCM 加密一次,等有人需要看的時候才臨時解開。帳號跟記錄之間也設計了一層隔離機制,每次查找或操作都只限定於自己擁有的資料範圍內。此外,API 回應不會去暴露那些後台專用的數據欄位,避免洩漏多餘資訊。
講到具體怎麼保護使用者資料,其實環節滿多。大致來說,加密都是在寫入資料庫之前完成;讀取時候則即時解開,而且只有當權限正確(通常得帶著 token)才行得通。有個 JWT 驗證攔截器,看起來是叫 requireAuth,在每次請求進來就檢查身份。最後,每次查詢語法都帶著 userId 條件,所以別人的東西基本碰不到。
測試 Vault API 時,用最簡單的方法:curl 指令,一個一個動作慢慢試。有些情境下,比如沒有帶驗證資訊直接發 POST 請求去建立新秘密——預期結果應該是被拒絕才合理。所以如果看到回傳大約四百多型態的未授權訊息,就知道系統防守至少沒明顯洞口。
進入第三階段,大致就是把重心轉到 Vault API,也就是那種加密秘密資料的倉庫。這塊功能主要是給已經通過驗證的人用來存放自己的加密筆記、密碼或其他機密資訊。不過,不只是單純存東西而已,還得考慮很多細節——包含嚴格控管誰可以動什麼資料、然後儲存的內容一定要經過妥善加密。前面搞好的 JWT 驗證、Sequelize 還有 TypeScript 設定,到這裡才算真正派上用場。
Vault 系統其實規劃了不少功能,大概有五六項:像是「新增秘密」(POST /vault),也能「查看所有秘密」(GET /vault)、或者只看其中某一條(GET /vault/:id)。如果哪天想改內容,那就走 PUT 方法(/vault/:id);不需要了,刪掉也是幾乎一樣的路徑,只是換成 DELETE。
每筆記錄都有它自己的限制。例如保存前,都會用 AES-256-GCM 加密一次,等有人需要看的時候才臨時解開。帳號跟記錄之間也設計了一層隔離機制,每次查找或操作都只限定於自己擁有的資料範圍內。此外,API 回應不會去暴露那些後台專用的數據欄位,避免洩漏多餘資訊。
講到具體怎麼保護使用者資料,其實環節滿多。大致來說,加密都是在寫入資料庫之前完成;讀取時候則即時解開,而且只有當權限正確(通常得帶著 token)才行得通。有個 JWT 驗證攔截器,看起來是叫 requireAuth,在每次請求進來就檢查身份。最後,每次查詢語法都帶著 userId 條件,所以別人的東西基本碰不到。
測試 Vault API 時,用最簡單的方法:curl 指令,一個一個動作慢慢試。有些情境下,比如沒有帶驗證資訊直接發 POST 請求去建立新秘密——預期結果應該是被拒絕才合理。所以如果看到回傳大約四百多型態的未授權訊息,就知道系統防守至少沒明顯洞口。

保險箱API誕生記:加密存取與嚴格權限控制
有時候,大家會需要透過某種方式,把自己的秘密存進一個線上保險箱。像是用 POST 去傳送資料,內容大致上會提到什麼 SSH 金鑰或一些好像蠻真的資料,只要帶著正確的通行證(就是那串看起來很長的代碼),通常就能成功放進去。有人測試這個流程時,好幾次都發現,只要授權沒問題,傳上去的都被接受了。
換個角度看,如果想把以前塞進去的那些秘密撈出來,方法也不算困難。有些人只要用 GET 指令,再搭配自己的身份識別,很快就能拿回屬於他們的一堆資料(其實還原後差不多都是明文)。不同使用者各自領回各自那一份,看起來沒有明顯交錯,也算安穩。
事情偶爾會複雜點。如果你嘗試去撈別人的單一秘密,比如說直接指定號碼,其實系統並不會讓你得逞。大部分時間,如果不是自己所有,大概就只得到找不到東西的訊息;但如果本身就是擁有者,那細節又都攤在眼前。
更新舊有秘密倒沒想像中複雜。有人試過用 PUT 方法再改一次內容,把標題與那些密碼類似的東西稍微動手腳。若通行證對得上,通常更新後系統會給點回應說完成了。但萬一不是自己那份,就算怎麼發指令,好像還是無法修改——頂多收到通知說未被授權吧。
整體下來,這套做法感覺偏向以「誰是誰」作為核心判斷,有些操作順利、有些則卡在授權這道關卡。不見得每個步驟都百分百精準,但在一般情境下,大約能達到該有的保護效果,只是偶爾可能還是漏掉小細節罷了。
換個角度看,如果想把以前塞進去的那些秘密撈出來,方法也不算困難。有些人只要用 GET 指令,再搭配自己的身份識別,很快就能拿回屬於他們的一堆資料(其實還原後差不多都是明文)。不同使用者各自領回各自那一份,看起來沒有明顯交錯,也算安穩。
事情偶爾會複雜點。如果你嘗試去撈別人的單一秘密,比如說直接指定號碼,其實系統並不會讓你得逞。大部分時間,如果不是自己所有,大概就只得到找不到東西的訊息;但如果本身就是擁有者,那細節又都攤在眼前。
更新舊有秘密倒沒想像中複雜。有人試過用 PUT 方法再改一次內容,把標題與那些密碼類似的東西稍微動手腳。若通行證對得上,通常更新後系統會給點回應說完成了。但萬一不是自己那份,就算怎麼發指令,好像還是無法修改——頂多收到通知說未被授權吧。
整體下來,這套做法感覺偏向以「誰是誰」作為核心判斷,有些操作順利、有些則卡在授權這道關卡。不見得每個步驟都百分百精準,但在一般情境下,大約能達到該有的保護效果,只是偶爾可能還是漏掉小細節罷了。
刻意留下後門:SSRF漏洞模擬實驗全記錄
如果說要刪除一個秘密,像這樣的指令其實還挺常見──用 curl 去打一個本地端的 API,帶著授權資訊然後對指定資源下 DELETE。成功與否,大致也就分成被允許或者不行,被拒絕通常只會丟個很模糊的訊息,看起來好像什麼都沒發生。有些經驗談很值得記下:例如每一筆資料都該做細緻到單行的存取管控,千萬不能只根據某個 ID 就信任對方;還有機密資料嘛,理論上應該在寫入前就先加密,不是事後才補。欸,有時候型別和錯誤檢查也不是雞肋,早點抓出問題其實省掉不少麻煩(尤其那種和 req.user 有關的小瑕疵)。另外,如果有人沒權限亂請求,最好讓這類失敗響應安靜又含糊一點,就回傳那種看起來像不存在的錯誤,不要明講「你不能看」這種話。
有段時間,在把 API 做得差不多之後,其實開始想看看反面教材──故意造一個小洞練習一下,也就是俗稱 SSRF 的風險。伺服器端請求偽造(Server-Side Request Forgery),大致上是攻擊者設法讓服務端跑去連一些原本該藏起來、不給外人碰的內部資源,比方雲端管理介面或某些管理後台。聽說這種問題偶爾會讓人頭疼。
模擬方式其實蠻直觀,新加了一條路徑叫 `/ssrf/fetch`。規則很簡單:來者自便,只要 query string 裡丟進一個網址參數,程式就直接幫你拿 axios 把東西撈回來──整包資料再丟還出去。檢查?沒有特別做,所以大家愛去哪裡連就去哪裡連。
範例邏輯大概長這樣:
但也正因為沒有任何限制,有心人想幹嘛基本都能通過……
有段時間,在把 API 做得差不多之後,其實開始想看看反面教材──故意造一個小洞練習一下,也就是俗稱 SSRF 的風險。伺服器端請求偽造(Server-Side Request Forgery),大致上是攻擊者設法讓服務端跑去連一些原本該藏起來、不給外人碰的內部資源,比方雲端管理介面或某些管理後台。聽說這種問題偶爾會讓人頭疼。
模擬方式其實蠻直觀,新加了一條路徑叫 `/ssrf/fetch`。規則很簡單:來者自便,只要 query string 裡丟進一個網址參數,程式就直接幫你拿 axios 把東西撈回來──整包資料再丟還出去。檢查?沒有特別做,所以大家愛去哪裡連就去哪裡連。
範例邏輯大概長這樣:
const target = req.query.url as string;
const response = await axios.get(target);
res.json({ status: response.status, headers: response.headers, data: response.data });
但也正因為沒有任何限制,有心人想幹嘛基本都能通過……

駭客思維測試:用curl暴力驗證每個安全環節
有時候,如果有人偷偷塞進像「http://localhost/internal/meta」這樣的網址,伺服器居然會自己去抓取,不管這個資源本來是不是只打算讓內部看。有些人為了測試這種情況,還真的設計了一條只有內部才曉得的小路徑:叫做「/internal/meta」,其實就是模仿雲端主機常見的那種元資料服務(類似 AWS EC2 的 Metadata Service)。內容大概長這樣:
{
"instanceId": "i-1234567890abcdef0",
"localHostname": "ip-172-31-16-139.ec2.internal",
"availabilityZone": "us-east-1a",
"region": "us-east-1",
"secretAdminToken": "SuperSecretAdminToken123!"
}
其實根本沒想過要公開出去——但就是因為 SSRF,那些原本只能在房間裡講的話,好像突然被人從門縫偷聽走。
測試起來也不是很複雜。記得當時用 curl 試了幾下:
一開始,直接戳「/internal/meta」,也就順利拿到那些敏感小細節。然後再換個方式,透過 SSRF,那就是「/ssrf/fetch?url=http://localhost:4000/internal/meta」這種寫法,結果等於請伺服器自己把內部東西送出來給外面看。最後也順便碰一下外部網站,比方說 example.com,一樣可以借伺服器的手出去撈資料回來。
現實中,其實不光是元資料而已,有些雲端服務(像什麼 169.254.169.254 那類)藏著不少臨時金鑰,有時候連管理頁面或內網資料庫都可能因此被摸到。有誰能百分之百確定,只要對方可以控制你伺服器去哪裡抓東西,就不會一步步往更深處繞?
大致搞完這整套下來,看起來做 API 好像不是特別難,但一些意想不到的小漏洞,有時候就能讓事情變得複雜許多。經驗談嘛,大概如此——不能完全說是哪個步驟保證安全,也只是提醒一下:有空還是多留意細節比較好。
{
"instanceId": "i-1234567890abcdef0",
"localHostname": "ip-172-31-16-139.ec2.internal",
"availabilityZone": "us-east-1a",
"region": "us-east-1",
"secretAdminToken": "SuperSecretAdminToken123!"
}
其實根本沒想過要公開出去——但就是因為 SSRF,那些原本只能在房間裡講的話,好像突然被人從門縫偷聽走。
測試起來也不是很複雜。記得當時用 curl 試了幾下:
一開始,直接戳「/internal/meta」,也就順利拿到那些敏感小細節。然後再換個方式,透過 SSRF,那就是「/ssrf/fetch?url=http://localhost:4000/internal/meta」這種寫法,結果等於請伺服器自己把內部東西送出來給外面看。最後也順便碰一下外部網站,比方說 example.com,一樣可以借伺服器的手出去撈資料回來。
現實中,其實不光是元資料而已,有些雲端服務(像什麼 169.254.169.254 那類)藏著不少臨時金鑰,有時候連管理頁面或內網資料庫都可能因此被摸到。有誰能百分之百確定,只要對方可以控制你伺服器去哪裡抓東西,就不會一步步往更深處繞?
大致搞完這整套下來,看起來做 API 好像不是特別難,但一些意想不到的小漏洞,有時候就能讓事情變得複雜許多。經驗談嘛,大概如此——不能完全說是哪個步驟保證安全,也只是提醒一下:有空還是多留意細節比較好。
從開發者變身守門員:這次實戰教會我的十件事
寫程式,好像從來不只是讓東西能跑就結束了。有些時候,腦袋得一邊站在工程師的角度,一邊模擬攻擊者的思路,心裡總會冒出「這裡到底會不會出事啊?」這種想法。這次做下來,有些細節真的記憶還蠻深刻。
JWT 身分驗證,看起來只要簽個 token 交出去好像就行,其實沒那麼單純。過期時間如果沒管好、簽章核對鬆散、refresh token 沒守住,大概很快就有資安風險冒出來。你說加密?資料丟進資料庫之前沒先處理成密文,等於是直接把祕密攤開給別人看,到時候誰要負責也說不準。資料只有真的用到時才解開,大致上比較安心。
型別語言 TypeScript 剛上手通常都卡卡的,但後面發現它多半能幫忙揪出那些原本測試才會炸出的 bug,雖然初期寫起來慢不少,不過長遠來看算是省麻煩吧。
權限控管呢,有人一開始覺得反正用戶自己送進來的東西應該沒差,結果常常就是這種想法害得資料被誤取甚至洩漏。每次查詢還是再 double check 一下,是不是當前登入的人自己的東西,比較保險。
SSRF 這玩意兒老實說很容易藏在一些表面單純的小功能背後,只要哪天讓用戶決定伺服器要連哪個網址,搞不好內部服務一下子就暴露了。如果有機會故意留下一點破口,再回頭去觀察設計上的安全性差異,那體會特別明顯——安全與否往往就在一些小細節。
整個過程其實不像一般專案那樣單純,就是做完而已,更像是一場小型實驗室練習,同時摸索怎麼打造,也順便體驗怎麼拆解跟防禦現代網路 API 的漏洞。有些觀念以前大概聽過,但動手之後感受又有點不同。不確定是不是所有情境都適用,不過至少在這些環節,多了一點警覺和理解。
#APIsecurity #APIdesign #SecureCoding #Nodejs #TypeScript #PostgreSQL #JWTauthentication #Encryption #CyberSecurity #WebAppSecurity #EthicalHacking #PenetrationTesting #BugBounty #SSRF #OWASPTop10 #BackendDevelopment #Sequelize #Expressjs #SecurityTesting #CTF
JWT 身分驗證,看起來只要簽個 token 交出去好像就行,其實沒那麼單純。過期時間如果沒管好、簽章核對鬆散、refresh token 沒守住,大概很快就有資安風險冒出來。你說加密?資料丟進資料庫之前沒先處理成密文,等於是直接把祕密攤開給別人看,到時候誰要負責也說不準。資料只有真的用到時才解開,大致上比較安心。
型別語言 TypeScript 剛上手通常都卡卡的,但後面發現它多半能幫忙揪出那些原本測試才會炸出的 bug,雖然初期寫起來慢不少,不過長遠來看算是省麻煩吧。
權限控管呢,有人一開始覺得反正用戶自己送進來的東西應該沒差,結果常常就是這種想法害得資料被誤取甚至洩漏。每次查詢還是再 double check 一下,是不是當前登入的人自己的東西,比較保險。
SSRF 這玩意兒老實說很容易藏在一些表面單純的小功能背後,只要哪天讓用戶決定伺服器要連哪個網址,搞不好內部服務一下子就暴露了。如果有機會故意留下一點破口,再回頭去觀察設計上的安全性差異,那體會特別明顯——安全與否往往就在一些小細節。
整個過程其實不像一般專案那樣單純,就是做完而已,更像是一場小型實驗室練習,同時摸索怎麼打造,也順便體驗怎麼拆解跟防禦現代網路 API 的漏洞。有些觀念以前大概聽過,但動手之後感受又有點不同。不確定是不是所有情境都適用,不過至少在這些環節,多了一點警覺和理解。
#APIsecurity #APIdesign #SecureCoding #Nodejs #TypeScript #PostgreSQL #JWTauthentication #Encryption #CyberSecurity #WebAppSecurity #EthicalHacking #PenetrationTesting #BugBounty #SSRF #OWASPTop10 #BackendDevelopment #Sequelize #Expressjs #SecurityTesting #CTF