這是 pre-signed URL 學習路線圖的 Level 5。前面幾站你已經知道:pre-signed URL 就是「把一個 HTTP 請求先簽好、塞進一條網址」,任何人拿到它就能在期限內下載或上傳一個私有物件,全程不帶帳密。功能面你已經會用了。這一站要補的是「安全直覺」——那些會讓你半夜被叫起來的坑。
本篇可獨立閱讀。每個陷阱都用同一個節奏講:症狀(你會看到什麼)→ 為什麼會這樣 → 怎麼避。讀完你會對「這條 URL 到底有多危險、能被鎖多緊」有踏實的判斷。
本站終點:給你一句需求(「讓外部廠商上傳報表,限 image/pdf、限 15 分鐘、只准公司網段」),你能當場說出:期限怎麼設、要不要綁 Content-Type、為什麼純 PUT 擋不住檔案大小、要不要上signatureAge/SourceIp條件。文末有這題的完整解答。
核心名詞先在這裡講定,其餘專有名詞(如 SignedHeaders、UNSIGNED-PAYLOAD)出現時會就地解釋。以下四個先用白話講白,讀下去就不卡:
? 後面那串叫 query string(如 ?X-Amz-Signature=abc...),或放在看不見的 header 裡。pre-signed URL 的簽章是放在 query string——這個選擇本身就埋了一個外洩風險,等下第三個陷阱會講。結論先講:能設多短就設多短,短到「剛好夠用」為止。pre-signed URL 是 bearer token,它活著的每一秒都是一個「誰撿到都能用」的窗口,期限越長、暴露面越大。
你能設的範圍(依產生方式不同):
| 產生方式 | 期限上限 |
|---|---|
| 用 AWS 主控台(console)點出來的 | 1 分鐘 ~ 12 小時 |
| 用 AWS CLI / SDK 程式產(且用 IAM user 長期憑證) | 最長 7 天(604800 秒) |
| 用 CLI / SDK 程式產,但簽章用的是臨時憑證(STS / role / EC2) | 會比 7 天更短——被憑證壽命壓著(見陷阱 2) |
那到底該設多短?AWS 的官方最佳實務指出:pre-signed URL 可以比 STS 臨時憑證還短——在條件對的情況下,60 秒都是可行的(STS 臨時憑證最短是 15 分鐘,presigned 沒有這個下限)。原則很簡單:這條 URL 預計多久內會被用掉,就設多久,別「反正設長一點比較保險」——那是反過來的,設長才不保險。
怎麼避:用「業務上合理的最短時間」當期限。下載連結給人點:幾分鐘到一小時。後端拿到就馬上用的:幾十秒到幾分鐘。不要無腦設 7 天。
症狀:你明明設了 Expires = 7 天,結果 URL 過一兩個小時就開始回 403 / ExpiredToken,怎麼算都不對。
為什麼:如果你是用臨時憑證(STS / IAM role / EC2 instance profile)簽出這條 URL,那 URL 的真正壽命是「你設的 expires」和「那組臨時憑證本身的壽命」兩者取較短的那個。AWS 白紙黑字:presigned URL「不可能比簽它的憑證活得更久」,就算你設更長的到期時間也一樣。
而臨時憑證的預設壽命往往很短:
| 簽 URL 用的憑證來源 | 它自己多久到期 |
|---|---|
STS AssumeRole | role session 結束就失效,預設 1 小時 |
| EC2 instance profile(機器 metadata 拿到的) | 會定期輪替,最長約 6 小時 |
| ECS task / 容器角色 | 通常 1~6 小時輪替一次 |
| IAM user 長期 access key | 不會自己過期(所以才能撐到 7 天) |
所以在 EC2 上跑的服務,拿 instance profile 簽一條「7 天」的 URL,實際上約 6 小時後就死了——因為那把臨時憑證輪替掉了。這是最常見的「為什麼我設 7 天卻提早死」。
怎麼避:(1) 心裡有數——臨時憑證簽出來的 URL,真正期限被憑證壓著。(2) 真的需要長效 URL,才用 IAM user 長期憑證來簽;但要清楚,長期 access key 本身就是一筆資安負債——它不會自動過期、一旦外洩得靠人工撤銷,AWS 的安全建議也是能不用就不用、優先改用臨時憑證。也就是說「用長期憑證換長效」是拿一個風險換另一個,不是免費的。(3) 需要 URL 一產出就馬上用掉,那臨時憑證完全 OK,因為你根本用不到那麼久。
結論:一條 pre-signed URL 外洩,就等於存取權外洩。它是 bearer token,撿到的人在期限內就能照著做那件事——下載你的私有檔、或往你 bucket 塞東西。
而它比一般憑證更容易外洩,原因在「簽章放在 query string」。AWS 官方點名這個風險:簽章是 X-Amz-Signature 這個 query 參數,而 query string 是網址的一部分——網址天生到處被記錄:伺服器 access log、瀏覽器歷史、頁面跳轉帶的 Referer header、公司 proxy 的流量記錄……都會把整條網址(含簽章)抄下來。相比之下,把秘密放在 HTTP header 反而比較少被記進 log。
官方原話重點:「Query string parameters ... 風險是 client 可能把 URI 連同簽章一起記進 log」。AWS 建議:記 log 時把X-Amz-Signature遮掉(redact),或整段 query string 遮掉,或把它當高機密處理。S3 自己的 server access log 與 CloudTrail 就是這樣做的——會保留 URI 但把X-Amz-Signature塗掉。
怎麼避:
X-Amz-Signature=[a-f0-9]{64} 整段替換成 X。症狀:你(或前端)把 URL 拿到手後,順手改了點東西——換個檔名、多加一個 query 參數、改個 header——馬上收到 SignatureDoesNotMatch。
為什麼:這不是 bug,是特性。server 收到請求時,會拿請求裡的 method、路徑、被簽進去的 header、query 參數重新算一次簽章,再跟 URL 上的 X-Amz-Signature 比對。只要有任何一個被簽進去的東西差了一個字元,重算出來的簽章就對不上 → 直接拒絕。
這正是 pre-signed URL 的安全保證:它鎖死了「只能做當初簽的那一件事」——不能把 GET 改成 DELETE、不能把 photo.jpg 改成 secret.pdf、不能偷加參數。想改任何一項,只能重新簽一條。
怎麼避:URL 產出來就原封不動地用。前端別「幫忙」加參數或改 header。AWS 排查 SignatureDoesNotMatch 的官方建議也包含這句:確認 method、headers、query string 在「產 URL」和「用 URL」兩端完全一致。(另一個隱形殺手是 proxy:有些公司 proxy 會偷改 header 或 query,導致簽章對不上——排查時先試著繞過 proxy。)
症狀:程式看起來都對,卻一直 SignatureDoesNotMatch,或 URL「一產出來就過期」。
為什麼:簽章跟時間綁得很死。URL 裡的 X-Amz-Date 是「簽章當下的時間」,到期判斷就從這裡起算。如果產 URL 的機器系統時間跟 S3 / MinIO 伺服器差太多(clock skew),server 會覺得這條 URL 的時間戳很可疑——太舊就當它過期、對不上驗證窗口就直接 SignatureDoesNotMatch。AWS 明說:就算只是很小的時間漂移,都可能讓簽章失效。
怎麼避:讓產 URL 的機器開 NTP 自動對時(跟時間伺服器同步)。這是 AWS 排查 SignatureDoesNotMatch 的第一條建議。容器 / 雲主機通常預設就有,自架環境要特別確認。
一句話:簽 URL 時如果把 Content-Type 也列進「要被簽章保護的 header 清單」(這份清單就叫 SignedHeaders),那上傳方送檔案時就必須帶一模一樣的 Content-Type,差一個字都會失敗。
好處(拿來當限制):你想「這條上傳 URL 只准用來傳 image/jpeg」,就把 Content-Type: image/jpeg 簽進去。對方要用這條 URL,header 就得帶 image/jpeg,否則 SignatureDoesNotMatch。
壞處(最常見的踩雷):前端上傳時沒帶對 header——你簽的是 image/jpeg,瀏覽器或前端框架自作主張送了 application/octet-stream 或別的,於是上傳一直失敗,還很難查,因為「程式看起來都對」。
# 若簽 URL 時綁了 Content-Type: image/jpeg,
# curl 上傳時就必須帶「一模一樣」的值,否則 SignatureDoesNotMatch
curl -X PUT --upload-file ./photo.jpg \
-H "Content-Type: image/jpeg" \
"<presigned-put-url>"
怎麼避:要不要綁 Content-Type,取決於你要不要用它當限制。要綁,就務必讓前端送出「與簽章時完全相同」的值,並把這件事寫進前後端的約定。不需要限制類型時,乾脆別把 Content-Type 簽進去,少一個對不上的來源。
註:Content-Type 有沒有被簽進 SignedHeaders,跟你用的 SDK 與呼叫方式有關(例如 AWS SDK for .NET 在 GetPreSignedUrlRequest 設了 ContentType 就會簽它)。以你們專案實際用的 SDK 行為為準。
結論:一條 presigned PUT URL 只綁三件事——HTTP method、物件 key、以及(可選的)被簽進去的 header。它沒有任何欄位可以限制「檔案多大」或「內容是什麼」。
為什麼:PUT 的簽章裡根本沒有「檔案大小」這個東西。簽 URL 時 S3 不把檔案內容算進簽章——那個本來要放「內容雜湊」的位置,pre-signed URL 一律填一個固定字串 UNSIGNED-PAYLOAD(意思就是「內容不簽」),所以你換內容、換大小,簽章照樣成立。這代表:拿到一條 presigned PUT URL 的人,可以往那個 key 塞任意大小、任意內容的檔案——你只綁了「傳去哪個 key」,沒綁「能傳什麼」。就算你靠陷阱 6 綁了 Content-Type,那也只擋得住「header 送錯」,擋不住「header 寫 image/jpeg 但內容其實是 10GB 的別的東西」。
怎麼避:真的需要限制「檔案大小上限 / key 前綴 / content-type 白名單」,不要用 presigned PUT,改用 presigned POST(policy-based upload)——它是把一份「policy」簽進去,可以強制大小範圍、前綴、類型等條件。(POST policy 的細節屬於 Level 7,這裡只要記得「純 PUT 限不了,要限就換 POST policy 或用 bucket policy 條件」。)
前面幾條都是「產 URL 這端」要注意的。但你還有第二道防線:在 policy 上加條件,讓 S3 在「收到請求」時再擋一次。這不受前端怎麼用 URL 影響,是伺服器端的硬規則。兩個最實用的條件:
s3:signatureAge:拒絕「太老的簽章」就算 URL 的 X-Amz-Expires 還沒到,你也可以要求「簽章產出來超過 N 毫秒就一律拒絕」,把外洩窗口再壓小。AWS 官方範例:簽章超過 10 分鐘(600000 毫秒)就 deny。
{
"Effect": "Deny",
"Principal": { "AWS": "*" },
"Action": "s3:*",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"NumericGreaterThan": { "s3:signatureAge": "600000" } // 毫秒;>10 分鐘就拒
}
}
別把門檻設太低(有上界,下界也要顧):簽章從產出到真正被用,中間要扣掉網路延遲加上少許時鐘誤差。門檻壓到只剩幾秒(例如照樣造句寫 5000=5 秒)會把正常請求也一併誤殺,反而製造難查的間歇性失敗。實務上留在數十秒以上(約 60 秒起跳)較安全——這是操作經驗的下界,不是官方硬性規定的數字。
aws:SourceIp / VPC 條件:限制來源想讓 URL「只准從公司網段用」,就用來源條件擋掉其他來源——這樣就算 URL 外洩到外網,別人也用不了。這條可以加在「發出請求的 IAM principal」上,也可以加在「bucket / access point」上——不是只能加 bucket(兩邊都設也行)。
aws:SourceIp(限 IP 網段)。aws:SourceVpc 或 aws:SourceVpce。AWS 官方的來源限制範例(掛在 principal 或 bucket 都成立):
{
"Sid": "NetworkRestrictionForIAMPrincipal",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"NotIpAddressIfExists": { "aws:SourceIp": "<公司 IP 網段>" },
"BoolIfExists": { "aws:ViaAWSService": "false" }
}
}
注意:這種來源限制不論加在 IAM principal 還是 bucket / access point,都會套用到所有對該資源的存取,不只 pre-signed URL 的情境——設之前要確認不會誤傷正常流量。
常有人問:「presigned URL 是不是 MinIO 特別容易被攻擊,S3 就還好?」先講結論:presigned 的簽章機制(SigV4)S3 跟 MinIO 是同一套,演算法本身沒有強弱之分(這點 Level 6 會講透可遷移性)。MinIO 的風險比較高,不是因為它「比較好破」,而是它通常被怎麼營運——爆炸半徑與偵測能力差一截。三個具體差別:
X-Amz-Credential 會明碼露出 access key ID,等於告訴攻擊者「這把(可能是 root 的)鑰匙叫什麼」;(3) 簽章用的那把 key 本身若外洩(被塞進 app/config),整個叢集淪陷。s3:signatureAge、aws:SourceIp、VPC endpoint)加上 CloudTrail、GuardDuty,在 AWS 上是成熟且開箱可用的一整套「限縮爆炸半徑 + 偵測」工具。MinIO 雖也有 policy/STS,但條件支援較少、也較少被實際設定,稽核不是開箱即用——外洩之後你既限縮不了、也不易察覺。怎麼避(MinIO 環境):
誠實標註:MinIO 確實也提供 bucket policy 條件與 STS 短憑證,上述「護欄較弱」指的是常見部署的預設與實務,不是 MinIO 完全做不到;你們能配置到多嚴,以實際採用的 MinIO 版本與設定為準。
| 你看到的症狀 | 最可能的病因 | 怎麼解 |
|---|---|---|
| URL 提早過期(設 7 天卻幾小時就死) | 用臨時憑證(STS / role / EC2)簽的,被憑證壽命壓著 | 要長效改用 IAM user 長期憑證(但那是資安負債);或接受短效即可 |
SignatureDoesNotMatch | URL 被改過 / header 沒帶對 / 時鐘偏移 / proxy 竄改 | URL 原封不動用、header 對齊、機器 NTP 對時、試繞過 proxy |
ExpiredToken | 簽 URL 的臨時憑證已失效 | 刷新憑證後重新產 URL |
| 上傳一直失敗但「看起來都對」 | 簽了 Content-Type,前端送出的值對不上 | 前端送出與簽章時完全相同的 Content-Type |
| 廠商用一條 PUT URL 傳了超大 / 錯誤內容 | 純 PUT 綁不了大小與內容 | 改用 presigned POST policy 或 bucket policy 條件 |
| URL 外洩被盜用 | 簽章在 query string,被 log / 歷史 / proxy 記下 | 期限設短、log 遮掉簽章、上 signatureAge / SourceIp |
| 用 MinIO:URL 外洩後果比 S3 大、也較難察覺 | 常用靜態 root 金鑰簽、缺 STS 自動過期與 IAM/CloudTrail 護欄、又要自己修 CVE | 改用低權限專用 service account 簽、能用 MinIO STS 就用短效、勤升版 |
需求:讓外部廠商上傳一份報表,限 image/pdf、限 15 分鐘、只准公司網段。逐項回答。
UNSIGNED-PAYLOAD),廠商可以往那個 key 塞任意大小的檔案(陷阱 7)。要真正限制「報表不能超過 X MB」和「只能 image/pdf」,正解是 presigned POST policy(可設 content-length-range 與類型白名單),不是 PUT。signatureAge / SourceIp 條件?SourceIp 要——「只准公司網段」正是它的用途,用 aws:SourceIp 擋掉公司網段以外的來源(走 VPC endpoint 就用 aws:SourceVpc(e))。這樣就算 URL 外洩到外網也用不了。signatureAge 可選:15 分鐘期限本身已經夠短,但若想再收緊「簽出來超過 N 分鐘就拒」,加一條 signatureAge 是免費的額外保險(記得門檻別壓到只剩幾秒)。