fix(security): SRI для CDN-скрипта сканера QR

Автоматическое ревью справедливо отметило загрузку html5-qrcode с CDN без
проверки целостности: подмена файла на стороне CDN исполнилась бы в кабинете
с полными правами страницы (доступ к сессии, initData Telegram).

Скрипт подключается с integrity (sha384 сверен по фактическому файлу
html5-qrcode@2.3.8), crossorigin=anonymous и referrerPolicy=no-referrer. При
несовпадении хеша браузер откажется исполнять скрипт, и сканер деградирует в
«камера недоступна» вместо запуска чужого кода.

Заодно закрыт тот же изъян в исходном месте, откуда пришёл паттерн:
TvQuickConnect грузил тот же файл с jsdelivr вообще без проверки. Он
переведён на общий хелпер (файлы на unpkg и jsdelivr побайтово идентичны —
сверил), дублирующийся загрузчик и интерфейс удалены.

Виджеты telegram.org намеренно оставлены без SRI: это самообновляемые
эндпоинты Telegram, фиксация хеша сломала бы логин при их обновлении.
This commit is contained in:
Fringg
2026-07-29 22:14:32 +03:00
parent 0e4dc43335
commit b615927283
2 changed files with 17 additions and 26 deletions

View File

@@ -12,6 +12,11 @@ import {
const TG_MOBILE_PLATFORMS = new Set(['ios', 'android', 'android_x', 'ios_x']);
export const HTML5_QRCODE_CDN = 'https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js';
// SRI-хеш ИМЕННО этого файла (sha384 от html5-qrcode@2.3.8/html5-qrcode.min.js).
// Без него подмена на стороне CDN исполнилась бы в кабинете с полными правами
// страницы. При несовпадении браузер откажется исполнять скрипт — сканер
// деградирует в «камера недоступна», а не запускает чужой код.
const HTML5_QRCODE_SRI = 'sha384-c9d8RFSL+u3exBOJ4Yp3HUJXS4znl9f+z66d1y54ig+ea249SpqR+w1wyvXz/lk+';
export interface Html5QrcodeInstance {
start: (
@@ -62,9 +67,14 @@ export async function loadHtml5Qrcode(): Promise<WindowWithHtml5['Html5Qrcode']
const script = document.createElement('script');
script.src = HTML5_QRCODE_CDN;
script.integrity = HTML5_QRCODE_SRI;
script.crossOrigin = 'anonymous';
script.referrerPolicy = 'no-referrer';
document.head.appendChild(script);
await new Promise<void>((resolve) => {
script.onload = () => resolve();
// onerror срабатывает и при провале проверки целостности — вызывающий
// получит undefined и покажет «камера недоступна».
script.onerror = () => resolve();
});
return w.Html5Qrcode;