chore: guard unsafe DOM HTML with ESLint #18
Enable no-unsanitized recommended rules and verify TypeScript DOM sink diagnostics.
This commit is contained in:
@@ -88,7 +88,7 @@ Knip, dependency rules приложения и stories, сборки прило
|
|||||||
`npm run lint` запускает ESLint с `--max-warnings=0`; `npm run lint:fix`
|
`npm run lint` запускает ESLint с `--max-warnings=0`; `npm run lint:fix`
|
||||||
применяет автофиксы. [Конфигурация](eslint.config.mjs) подключает рекомендованные
|
применяет автофиксы. [Конфигурация](eslint.config.mjs) подключает рекомендованные
|
||||||
наборы всех установленных плагинов: TypeScript, React, Hooks, Refresh, Query,
|
наборы всех установленных плагинов: TypeScript, React, Hooks, Refresh, Query,
|
||||||
Unicorn, import-x, SonarJS, RegExp, Security, Perfectionist, Stylistic,
|
Unicorn, import-x, SonarJS, RegExp, Security, no-unsanitized, Perfectionist, Stylistic,
|
||||||
Vitest, Testing Library, jest-dom и Storybook. Правила React, тестов и stories
|
Vitest, Testing Library, jest-dom и Storybook. Правила React, тестов и stories
|
||||||
применяются к соответствующим файлам; generated/build artifacts исключены.
|
применяются к соответствующим файлам; generated/build artifacts исключены.
|
||||||
|
|
||||||
@@ -98,6 +98,8 @@ Stylistic отвечает за форматирование JS/TS/TSX, Perfecti
|
|||||||
`lint-staged` исправляет JS/TS/TSX перед commit. `lint:eslint` сначала собирает
|
`lint-staged` исправляет JS/TS/TSX перед commit. `lint:eslint` сначала собирает
|
||||||
private packages, чтобы анализ типов использовал актуальные declarations.
|
private packages, чтобы анализ типов использовал актуальные declarations.
|
||||||
`npm run test:lint` проверяет действующие presets и ошибочные примеры.
|
`npm run test:lint` проверяет действующие presets и ошибочные примеры.
|
||||||
|
`no-unsanitized` проверяет опасные DOM HTML-присваивания и вызовы;
|
||||||
|
React `dangerouslySetInnerHTML` отдельно проверяется React-плагином.
|
||||||
|
|
||||||
Точечные исключения в исходниках объясняют контракты и ограничения статического
|
Точечные исключения в исходниках объясняют контракты и ограничения статического
|
||||||
анализа; изменение nullable API fields, query keys или порядка race-операций
|
анализа; изменение nullable API fields, query keys или порядка race-операций
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Security-плагины ESLint
|
||||||
|
|
||||||
|
Проверено 28 сентября 2026 года для frontend: ESLint 10.11.0,
|
||||||
|
TypeScript и React 19. Рекомендация — добавить только
|
||||||
|
`eslint-plugin-no-unsanitized@4.1.5` с полным `configs.recommended`.
|
||||||
|
Решение реализовано: пакет добавлен в devDependencies, полный recommended
|
||||||
|
подключён в ESLint. Ниже сохранены результаты исследования до установки.
|
||||||
|
|
||||||
|
## Все пакеты из раздела Security
|
||||||
|
|
||||||
|
Источник списка: [awesome-eslint / Security](https://github.com/dustinspecker/awesome-eslint#security).
|
||||||
|
Версии и даты ниже — опубликованные `latest` из npm на момент проверки.
|
||||||
|
Широкий peer range сам по себе не доказывает работоспособность.
|
||||||
|
|
||||||
|
| Пакет | Версия; публикация | Заявленный peer ESLint | Решение для frontend |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [no-unsanitized](https://github.com/mozilla/eslint-plugin-no-unsanitized) | [4.1.5](https://registry.npmjs.org/eslint-plugin-no-unsanitized/4.1.5); 2026-02-19 | `^9 \|\| ^10` | Добавить: проверяет опасные присваивания и вызовы native DOM HTML API. Flat recommended включает `property` и `method` как errors. |
|
||||||
|
| [no-secrets](https://github.com/nickdeis/eslint-plugin-no-secrets) | [2.3.3](https://registry.npmjs.org/eslint-plugin-no-secrets/2.3.3); 2026-03-01 | `>=5` | Не добавлять: поиск секретов по entropy/patterns существенно пересекается с существующими Gitleaks и SonarJS. Flat config поддерживается; отсутствие рекомендации связано с дублированием. |
|
||||||
|
| [pii](https://github.com/shiva-hack/eslint-plugin-pii) | [1.0.2](https://registry.npmjs.org/eslint-plugin-pii/1.0.2); 2021-04-13 | `>=4.19.1` | Не добавлять: правило падает на ESLint 10. Проверяет email/даты рождения/IP/телефоны в литералах и комментариях, а не потоки персональных данных; потребует исключений для тестовых данных. |
|
||||||
|
| [pg](https://github.com/interlace-collie/eslint/tree/main/packages/eslint-plugin-pg) | [1.4.14](https://registry.npmjs.org/eslint-plugin-pg/1.4.14); 2026-08-06 | `^8.0.0 \|\| ^9.0.0 \|\| ^10.0.0` | Не добавлять: предназначен для Node.js с `pg`, которого во frontend нет. Пакет deprecated и переименован в `eslint-plugin-postgresql-security`; старое имя больше не обновляется. |
|
||||||
|
| [security](https://github.com/eslint-community/eslint-plugin-security) | [4.1.0](https://registry.npmjs.org/eslint-plugin-security/4.1.0); 2026-09-27 | Не указан | Уже установлен с recommended. Ориентирован главным образом на Node.js; не заменяет проверки DOM. |
|
||||||
|
| [xss](https://github.com/Rantanen/eslint-plugin-xss) | [0.1.12](https://registry.npmjs.org/eslint-plugin-xss/0.1.12); 2022-06-27 | Не указан | Не добавлять: правило падает на ESLint 10. Проверяет HTML-конкатенации/соглашения об именах и присваивания `location.href`; это не специализированная проверка React/TSX. |
|
||||||
|
|
||||||
|
## Что добавляет no-unsanitized
|
||||||
|
|
||||||
|
[Правило property](https://github.com/mozilla/eslint-plugin-no-unsanitized/blob/master/docs/rules/property.md)
|
||||||
|
проверяет присваивания `innerHTML`/`outerHTML`;
|
||||||
|
[method](https://github.com/mozilla/eslint-plugin-no-unsanitized/blob/master/docs/rules/method.md)
|
||||||
|
проверяет, в частности, `insertAdjacentHTML`, `document.write` и `document.writeln`.
|
||||||
|
В [исходнике method](https://github.com/mozilla/eslint-plugin-no-unsanitized/blob/master/lib/rules/method.js)
|
||||||
|
также есть проверки `createContextualFragment`, `setHTMLUnsafe` и dynamic import.
|
||||||
|
Следовательно, перед включением нужно прогнать весь lint, а не только HTML-примеры.
|
||||||
|
|
||||||
|
До подключения no-unsanitized [eslint.config.mjs](eslint.config.mjs) уже включал
|
||||||
|
React-проверку `dom-no-dangerously-set-innerhtml` и security-правила SonarJS.
|
||||||
|
Предварительная проверка через локальный `ESLint.lintText` показала пробел:
|
||||||
|
функция с `element.innerHTML = html` не получала security-диагностику,
|
||||||
|
тогда как JSX `dangerouslySetInnerHTML` получал
|
||||||
|
диагностику React. Это различие в покрытии, не обнаруженная уязвимость приложения:
|
||||||
|
таких production DOM-присваиваний при поиске не найдено.
|
||||||
|
|
||||||
|
Плагин понимает TS-синтаксис через TypeScript parser, включая `as`-выражения,
|
||||||
|
но не использует TypeScript type checker. Он не доказывает безопасность всех
|
||||||
|
потоков данных. У версии 4.1.5 нет runtime dependencies; Prettier указан только
|
||||||
|
в devDependencies самого плагина и при его обычной установке потребителем
|
||||||
|
не устанавливается.
|
||||||
|
|
||||||
|
## Проверка совместимости без установки
|
||||||
|
|
||||||
|
Опубликованные npm tarballs прочитаны в `/tmp`; использованы локальные
|
||||||
|
ESLint 10.11.0 и `@typescript-eslint/parser`, без lifecycle-скриптов пакетов.
|
||||||
|
Через `Linter.verify` с recommended no-unsanitized проверены:
|
||||||
|
|
||||||
|
- `innerHTML`/`outerHTML` с параметром `html: string` — обе ошибки найдены;
|
||||||
|
- `insertAdjacentHTML`/`document.write`/`document.writeln` — все три ошибки найдены;
|
||||||
|
- `innerHTML = html as string` — ошибка найдена;
|
||||||
|
- безопасный `textContent` и обычная TSX-интерполяция — без ошибок.
|
||||||
|
|
||||||
|
Отдельный запуск реальных правил `pii/no-email` и
|
||||||
|
`xss/no-location-href-assign` завершился
|
||||||
|
`TypeError: context.getSourceCode is not a function` на ESLint 10.
|
||||||
|
Это подтверждает несовместимость проверенных версий, несмотря на широкий
|
||||||
|
или отсутствующий peer range. Обходы через legacy adapters не предлагаются.
|
||||||
|
|
||||||
|
Поиск секретов уже выполняют [security workflow](.gitea/workflows/security.yml)
|
||||||
|
и pre-commit Gitleaks; SonarJS дополнительно проверяет hardcoded secrets.
|
||||||
|
Добавление ещё одного secret-scanner в ESLint сейчас не оправдано.
|
||||||
|
|
||||||
|
Активацию обоих правил и примеры опасных DOM-вставок проверяет
|
||||||
|
`npm run test:lint`; интеграцию зависимости — полный `npm run verify`.
|
||||||
@@ -6,6 +6,7 @@ import vitest from "@vitest/eslint-plugin";
|
|||||||
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
||||||
import { importX } from "eslint-plugin-import-x";
|
import { importX } from "eslint-plugin-import-x";
|
||||||
import jestDom from "eslint-plugin-jest-dom";
|
import jestDom from "eslint-plugin-jest-dom";
|
||||||
|
import noUnsanitized from "eslint-plugin-no-unsanitized";
|
||||||
import perfectionist from "eslint-plugin-perfectionist";
|
import perfectionist from "eslint-plugin-perfectionist";
|
||||||
import hooks from "eslint-plugin-react-hooks";
|
import hooks from "eslint-plugin-react-hooks";
|
||||||
import refresh from "eslint-plugin-react-refresh";
|
import refresh from "eslint-plugin-react-refresh";
|
||||||
@@ -32,6 +33,7 @@ export default defineConfig(
|
|||||||
sonarjs.configs.recommended,
|
sonarjs.configs.recommended,
|
||||||
regexp.configs.recommended,
|
regexp.configs.recommended,
|
||||||
security.configs.recommended,
|
security.configs.recommended,
|
||||||
|
noUnsanitized.configs.recommended,
|
||||||
perfectionist.configs["recommended-natural"],
|
perfectionist.configs["recommended-natural"],
|
||||||
stylistic.configs.customize({ indent: "tab", jsx: true, quotes: "double", semi: true }),
|
stylistic.configs.customize({ indent: "tab", jsx: true, quotes: "double", semi: true }),
|
||||||
],
|
],
|
||||||
|
|||||||
Generated
+11
@@ -62,6 +62,7 @@
|
|||||||
"eslint-import-resolver-typescript": "4.4.5",
|
"eslint-import-resolver-typescript": "4.4.5",
|
||||||
"eslint-plugin-import-x": "4.17.1",
|
"eslint-plugin-import-x": "4.17.1",
|
||||||
"eslint-plugin-jest-dom": "5.10.1",
|
"eslint-plugin-jest-dom": "5.10.1",
|
||||||
|
"eslint-plugin-no-unsanitized": "4.1.5",
|
||||||
"eslint-plugin-perfectionist": "5.12.1",
|
"eslint-plugin-perfectionist": "5.12.1",
|
||||||
"eslint-plugin-react-hooks": "7.1.1",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"eslint-plugin-react-refresh": "0.5.7",
|
"eslint-plugin-react-refresh": "0.5.7",
|
||||||
@@ -8862,6 +8863,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-plugin-no-unsanitized": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^9 || ^10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-plugin-perfectionist": {
|
"node_modules/eslint-plugin-perfectionist": {
|
||||||
"version": "5.12.1",
|
"version": "5.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.12.1.tgz",
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
"eslint-import-resolver-typescript": "4.4.5",
|
"eslint-import-resolver-typescript": "4.4.5",
|
||||||
"eslint-plugin-import-x": "4.17.1",
|
"eslint-plugin-import-x": "4.17.1",
|
||||||
"eslint-plugin-jest-dom": "5.10.1",
|
"eslint-plugin-jest-dom": "5.10.1",
|
||||||
|
"eslint-plugin-no-unsanitized": "4.1.5",
|
||||||
"eslint-plugin-perfectionist": "5.12.1",
|
"eslint-plugin-perfectionist": "5.12.1",
|
||||||
"eslint-plugin-react-hooks": "7.1.1",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"eslint-plugin-react-refresh": "0.5.7",
|
"eslint-plugin-react-refresh": "0.5.7",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import vitest from "@vitest/eslint-plugin";
|
|||||||
import { ESLint } from "eslint";
|
import { ESLint } from "eslint";
|
||||||
import { importX } from "eslint-plugin-import-x";
|
import { importX } from "eslint-plugin-import-x";
|
||||||
import jestDom from "eslint-plugin-jest-dom";
|
import jestDom from "eslint-plugin-jest-dom";
|
||||||
|
import noUnsanitized from "eslint-plugin-no-unsanitized";
|
||||||
import perfectionist from "eslint-plugin-perfectionist";
|
import perfectionist from "eslint-plugin-perfectionist";
|
||||||
import hooks from "eslint-plugin-react-hooks";
|
import hooks from "eslint-plugin-react-hooks";
|
||||||
import refresh from "eslint-plugin-react-refresh";
|
import refresh from "eslint-plugin-react-refresh";
|
||||||
@@ -71,6 +72,16 @@ test("ESLint enforces the frontend profile and excludes generated artifacts", as
|
|||||||
"import { exec } from \"node:child_process\"; exec(process.argv[2]);",
|
"import { exec } from \"node:child_process\"; exec(process.argv[2]);",
|
||||||
"security/detect-child-process",
|
"security/detect-child-process",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"src/shared/api/problem-details.ts",
|
||||||
|
"export function renderHtml(element: HTMLElement, html: string) { element.innerHTML = html; }",
|
||||||
|
"no-unsanitized/property",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"src/shared/api/problem-details.ts",
|
||||||
|
"export function renderHtml(element: HTMLElement, html: string) { element.insertAdjacentHTML(\"beforeend\", html); }",
|
||||||
|
"no-unsanitized/method",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"packages/ui/src/EntryCard/EntryCard.test.tsx",
|
"packages/ui/src/EntryCard/EntryCard.test.tsx",
|
||||||
"import { expect } from \"vitest\"; expect(document.activeElement).toBe(document.body);",
|
"import { expect } from \"vitest\"; expect(document.activeElement).toBe(document.body);",
|
||||||
@@ -126,6 +137,7 @@ test("all recommended plugin rules remain enabled in their intended scopes", asy
|
|||||||
[sonarjs.configs.recommended, source],
|
[sonarjs.configs.recommended, source],
|
||||||
[regexp.configs.recommended, source],
|
[regexp.configs.recommended, source],
|
||||||
[security.configs.recommended, source],
|
[security.configs.recommended, source],
|
||||||
|
[noUnsanitized.configs.recommended, source],
|
||||||
[perfectionist.configs["recommended-natural"], source],
|
[perfectionist.configs["recommended-natural"], source],
|
||||||
[stylistic.configs.recommended, component],
|
[stylistic.configs.recommended, component],
|
||||||
[react.configs["recommended-type-checked"], component],
|
[react.configs["recommended-type-checked"], component],
|
||||||
|
|||||||
Reference in New Issue
Block a user