Безопасность
Наша модель безопасности в одном предложении: на сервере нечего атаковать. Всё нижесказанное можно проверить через DevTools вашего браузера.
Архитектура
Генератор, это статическое одностраничное приложение, раздаваемое с Cloudflare Pages: нет учётных записей пользователей, аутентификации и кода бэкенда где-либо в процессе генерации. Каждая операция генерации QR, кодирования, сканирования и рендеринга выполняется полностью в вашем браузере. Один небольшой серверлесс-эндпоинт существует рядом с ним и указан здесь открыто, а не спрятан: /api/contact (форма обратной связи, которая сохраняет то, что вы отправляете, в Cloudflare D1 и отправляет это нам по почте). Он не вызывается, пока вы создаёте QR-код; проверка безопасности URL, это отдельный продукт на check.qr.abundera.ai. Проверить безопасность QR-кода
Модель угроз
Поскольку генератор не собирает, не хранит и не передаёт никаких пользовательских данных (два эндпоинта выше обрабатывают только то, что вы явно им отправляете), наиболее распространённые угрозы для веб-приложений, кража учётных данных, утечка базы данных, перехват сессий, серверные инъекции, к нему неприменимы. Оставшаяся поверхность атаки, это пакет статических ресурсов (HTML, CSS, JavaScript), раздаваемый с нашего источника. При проектировании мы исходим из следующего:
- Компрометация источника: злоумышленник подменяет один из наших bundled-ресурсов вредоносным. Приведённый ниже CSP ограничивает радиус поражения; токены cache-buster упрощают откат к заведомо исправной версии.
- Пользовательский ввод: QR-данные, имена vCard, пароли WiFi, строки CSV в пакетном режиме, содержимое отсканированных изображений. Каждый путь ввода считается ненадёжным при рендеринге обратно в DOM.
- Изображения от пользователя: URL фотографий vCard и загружаемые логотипы. Изображения отрисовываются на canvas и никогда не встраиваются в DOM как необработанная разметка.
- Сторонние расширения браузера: вне области ответственности. Если расширение в вашем браузере имеет разрешение на изменение каждой страницы, оно может изменить и нашу. Наши гарантии действуют при загрузке страницы в неизменённом виде.
Content Security Policy, по директивам
Политика ниже считывается из развёрнутого файла _headers при сборке этой страницы, поэтому она не может разойтись с тем, что отдаёт edge-сервер. Проверьте её в заголовках ответа любого запроса.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://abundera.ai 'wasm-unsafe-eval' https://challenges.cloudflare.com;
worker-src 'self' blob:;
style-src 'self' 'unsafe-inline' https://abundera.ai;
font-src 'self';
img-src 'self' data: blob: https:;
connect-src 'self' https: https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self'Что разрешает каждая директива и где она идёт на компромисс:
default-src 'self', жёсткий пол. Всё, что мы явно не ослабляем, остаётся в рамках одного источника.script-src 'self' https://abundera.ai 'wasm-unsafe-eval' https://challenges.cloudflare.com, никаких встроенных<script>, никакогоeval().abundera.ai, это наш собственный родительский домен, отдающий один общий скрипт, переключатель языка.challenges.cloudflare.com, это виджет Turnstile, используемый только на /contact/.'wasm-unsafe-eval'требуется для пути компиляции WebAssembly нашей библиотеки QR-кодирования; традиционныйeval()при этом НЕ разрешается. Наша проверка перед деплоем сканирует каждую HTML-страницу на наличие встроенных скриптов и прерывает сборку при их обнаружении.style-src 'self' 'unsafe-inline', реальная уступка. Предпросмотр QR вычисляет пиксельные цвета встроенно (атрибуты стилей на отдельных модулях). Хэш-based список разрешений работал бы, но обновление любого стиля без деплоя вызывало бы сбой. Компромисс: мы принимаем чуть более слабую политику стилей; стилизация всё равно не может утекать данные (у CSS нет доступа кconnect-src).img-src 'self' data: blob: https:,data:для встроенного рендеринга QR,blob:для URL загрузки при экспорте,https:для URL фотографий и аватаров vCard от пользователя. URL от пользователя никогда не выполняются, только отображаются. Учтите, что эта директива, это ещё и исходящий канал: любой скрипт, запущенный на странице, может запросить изображение с любого HTTPS-хоста и передать данные в URL. Мы принимаем это, потому что функции это нужно, и говорим об этом здесь прямо, вместо того чтобы называть политику непроницаемой.connect-src 'self' https: https://challenges.cloudflare.com, вот эту стоит прочитать внимательно.https:, это источник-схема: он позволяетfetch(), XMLHttpRequest, WebSocket по TLS, EventSource иsendBeacon()обращаться к любому HTTPS-источнику, а не только к тем, что вы вписали. Она нужна, чтобы поле фотографии vCard и проверка безопасности URL могли запросить адрес, который вы вставили. Она не предотвращает эксфильтрацию вредоносным скриптом, ни одна директива CSP не может этого, если скрипт уже выполняется от нашего имени. Эксфильтрацию предотвращает то, что наш поставляемый код не делает таких запросов, и это можно увидеть на вкладке Network.frame-src 'self' https://challenges.cloudflare.com, во фрейм можно поместить только виджет Turnstile.frame-ancestors 'none', ни один другой сайт не может встроить нас. Предотвращает clickjacking.base-uri 'self', внедрённый вредоносный тег<base>не сможет перенаправить относительные URL на источник злоумышленника.form-action 'self', любая внедрённая форма может отправлять данные только на наш источник. Единственная форма, которая куда-либо отправляет данные, это форма обратной связи.
Чего CSP не может
CSP ограничивает, откуда браузер загружает код. Он не проверяет, что делает уже загруженный код. Если бы наш origin оказался скомпрометирован, первый пункт в модели угроз, скрипт злоумышленника выполнялся бы с теми же правами, что и наш, включая допуски connect-src и img-src выше. Наша защита от этого лежит выше по цепочке, чем CSP: деплои идут через один аккаунт Cloudflare со скоуп-токенами, имена файлов ресурсов хешируются по содержимому, поэтому подменённый файл меняет каждый URL, который на него ссылается, а сервис-воркер кэширует только GET-ответы того же источника. Манифест описывает сайт таким, каким он поставляется. Это не утверждение, что никакая веб-страница никогда не сможет куда-либо отправить данные, и мы предпочитаем, чтобы вы держали нас в рамках именно этого более узкого, проверяемого утверждения.
К /bio/* (ослабленный img-src для аватаров от пользователей) и /embed/* (ослабленный frame-ancestors для намеренного встраивания) применяются отдельные политики CSP. Обе задокументированы в site/_headers.
Заголовки транспорта и фреймирования
- HSTS:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload, 1 год, все поддомены, соответствует требованиям HSTS preload list. Переход на HTTP отклоняется совместимыми браузерами. - X-Frame-Options: DENY, избыточен при наличии CSP
frame-ancestors, оставлен для поддержки старых браузеров. - X-Content-Type-Options: nosniff, предотвращает атаки MIME confusion.
- Referrer-Policy: strict-origin-when-cross-origin, при переходе по внешним ссылкам передаётся только источник, но не путь.
- Permissions-Policy:
camera=(self), microphone=(), geolocation=(), камера разрешена только для нашего собственного сканера; микрофон и геолокация явно запрещены даже при встраивании.
Сервис-воркер
Наш сервис-воркер (site/sw.js) кэширует только ресурсы из того же источника. Обработчик fetch явно отклоняет кросс-доменные запросы и методы, отличные от GET, логику можно прочитать в /sw.js (исходник поставляется достаточно неминифицированным, чтобы его можно было проследить). Запись в кэш обёрнута в event.waitUntil(), поэтому она не может быть прервана в процессе навигации.
Санитизация входных данных
Каждый путь рендеринга, принимающий пользовательский ввод, обрабатывает его как ненадёжный текст:
- Предпросмотр QR-данных использует
textContent, а неinnerHTML. - Цели шаринга (буфер обмена,
navigator.share()) передают текст пользователя как строку, никогда как разметку. - SVG-экспорт формируется нашей библиотекой кодирования; пользовательский контент кодируется в base64 в
xlink:hrefэлемента<image>, а не внедряется как SVG-элементы. - Предпросмотр печати использует blob URL, а не
document.write(). - Разбор localStorage обёрнут в
try/catch, повреждённая запись возвращает чистое пустое значение по умолчанию, а не исключение, способное раскрутить живой путь кода. - URL от пользователей, отображаемые как кнопки "Открыть ссылку", ограничены схемой
http(s)://, схемыjavascript:иdata:отклоняются.
Загрузка изображений с внешних ресурсов
Когда пользователь вставляет URL вида https: в качестве фотографии vCard или логотипа, браузер загружает его с учётом CORS и списка разрешений img-src нашего CSP. Изображение отрисовывается на canvas. Оно никогда не становится живым DOM, не выполняется как код и не достигает нашего источника, запрос идёт от браузера к удалённому изображению, а результат отображается на стороне клиента. Злоумышленник, контролирующий URL удалённого изображения, может отследить факт загрузки URL (строка в логе собственного сервера), но не способен извлечь что-либо с нашей страницы.
Subresource Integrity (SRI)
Весь JavaScript и CSS, который мы поставляем, является одноисточниковым. Мы не загружаем сторонние скрипты или таблицы стилей, поэтому SRI-хэши не применимы. Если мы когда-либо загрузим сторонний ресурс, мы добавим к нему атрибут SRI integrity и задокументируем процесс обновления хэша на этой странице.
Сообщение об уязвимости
Если вы обнаружили проблему безопасности, затрагивающую Abundera QR, в нашем коде, развёртывании или используемых зависимостях, пожалуйста, сообщите об этом конфиденциально по адресу security@abundera.ai. Наша цель, провести оценку в течение 72 часов. Вы также можете связаться с нами через контактные данные в файле /.well-known/security.txt.
Программы вознаграждений пока нет
В настоящее время мы не предлагаем денежных вознаграждений, но каждый подтверждённый действительный отчёт получает упоминание в журнале изменений и нашу публичную благодарность.
Проверьте всё вышесказанное
Каждое утверждение на этой странице можно проверить через DevTools браузера, не доверяя нам на слово:
- CSP: DevTools → Network →
/→ Headers. Прочитайте заголовок ответаContent-Security-Policy. - HSTS и заголовки безопасности: то же место, все
Strict-Transport-Security,X-Frame-Optionsи другие видны там. - Нет исходящих запросов: DevTools → Network → Fetch/XHR. Сгенерируйте QR. Убедитесь, что счётчик остаётся на нуле.
- Область сервис-воркера: DevTools → Application → Service Workers. Проверьте источник скрипта и список кэшированных ресурсов.
- Нет куков: DevTools → Application → Cookies. Пусто.
- Полное руководство: раздел верификации манифеста.
Контакты
Сообщения об уязвимостях: security@abundera.ai