Files
local-deep-research/eslint.config.js
LearningCircuit 0e9f86051a chore(lint): add eslint-plugin-regexp + fix 9 regex issues (incl 3 ReDoS) (#3662)
* chore(lint): add eslint-plugin-regexp + fix 9 regex issues (incl 3 ReDoS)

Adds eslint-plugin-regexp's recommended preset (~67 rules covering
regex anti-patterns and ReDoS hazards). Single dev dep.

### Real findings — 3 potential ReDoS vulnerabilities

`regexp/no-super-linear-backtracking` flagged 6 sites where regex
quantifiers can exchange characters with each other, causing
polynomial or exponential backtracking on adversarial input. Fixed:

- components/results.js — markdown heading parser
  `/^#\s*([^\n]+)[\s\S]*?^##\s*([^\n]+)/m`. The `\s*` could consume
  newlines and overlap with `[^\n]+`. Replaced with
  `/^#[ \t]+(\S[^\n]*)\n[\s\S]*?^##[ \t]+(\S[^\n]*)/m` — required
  space after #, captured text starts with non-whitespace, explicit
  newline anchor.
- tests/ui_tests/test_benchmark_ci.js — `(\d+\.?\d*)` rewritten as
  `(\d+(?:\.\d*)?)` to remove ambiguity.
- tests/ui_tests/test_context_overflow_ci.js (2 sites),
  test_metrics_dashboard_ci.js — same pattern fixes.

Even though these regexes parse server-generated content (low
practical exploitability), defensive fixes are cheap and keep the
rule active to catch any future user-input regex.

### Other fixes (6)

- regexp/no-dupe-characters-character-class: `[^\w_]` → `[^\w]`
  (mobile-navigation.js — `_` already in `\w`).
- regexp/use-ignore-case (4 sites): `[^a-zA-Z0-9-]` → `[^a-z0-9-]/i`
  in subscriptions.js, url-validator.js, test_lib/test_results.js,
  test_urls.test.js.
- regexp/prefer-w: `[a-zA-Z0-9_]` → `\w` in subscriptions.js.
- regexp/no-unused-capturing-group (5 sites): switched to
  non-capturing groups in test files where capture groups weren't
  used.
- regexp/optimal-quantifier-concatenation: `\d+[\d,]*` → `\d[\d,]*`
  in test_context_overflow_ci.js.
- regexp/no-useless-flag: dropped redundant `i` flag on a `\w`-only
  pattern in subscriptions.js.

All regex changes verified semantically equivalent — same input
sets match before and after.

### Dep cost

eslint-plugin-regexp@3.1.0 — ~550KB unpacked, single peer dep
(eslint >=9.38). Standard, well-maintained. Already had similar
single-purpose plugins (no-unsanitized, chai-friendly).

Verified: pre-commit eslint passes, 0 errors.

* fixup: convert capturing group to non-capturing in vite.config.js

Pre-commit's full-tree run catches vite.config.js (which my src/+tests/
local scan didn't include). The /\.(woff2?|ttf|eot|svg)$/ pattern is
used as a .test() check — the captured group is never accessed, so
non-capturing (?:...) is correct.
2026-04-26 10:07:39 +02:00

423 lines
15 KiB
JavaScript

// ESLint flat config for JavaScript files
// https://eslint.org/docs/latest/use/configure/configuration-files-new
import nounsanitized from "eslint-plugin-no-unsanitized";
import chaiFriendly from "eslint-plugin-chai-friendly";
import regexp from "eslint-plugin-regexp";
// Recognized safe escape/sanitize methods for no-unsanitized plugin.
// Implementations:
// escapeHtml, esc — security/xss-protection.js (escapes & < > " ')
// DOMPurify.sanitize — app.js imports dompurify, exposed as window.DOMPurify
// sanitizeHtml/sanitizeHTML — security/xss-protection.js / utils/sanitizer.js
const escapeConfig = {
escape: {
methods: [
"escapeHtml",
"esc",
"DOMPurify.sanitize",
"window.DOMPurify.sanitize",
"sanitizeHtml",
"sanitizeHTML",
"window.escapeHtml",
"window.sanitizeHtml",
"window.XSSProtection.escapeHtml",
],
},
};
export default [
// Global ignores — must be a standalone object (no "files" key) for ESLint flat config
{
ignores: [
"**/node_modules/**",
"**/static/dist/**",
"tests/ldr-news-dev-files/**",
"dist/**",
"build/**",
"**/*.min.js",
],
},
// Catch regex anti-patterns and ReDoS hazards. Recommended preset only —
// ~67 rules, all the safety-relevant ones already in the project's verified
// zero/near-zero violation profile.
regexp.configs["flat/recommended"],
{
// Apply to all JavaScript files
files: ["**/*.js", "**/*.mjs"],
plugins: {
"no-unsanitized": nounsanitized,
"chai-friendly": chaiFriendly,
},
rules: {
"no-undef": "error", // globals enumerated below in languageOptions.globals
"no-unused-vars": ["warn", {
"args": "after-used",
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"ignoreRestSiblings": true,
}],
// --- High priority: real bugs found in codebase ---
"no-var": "error",
"no-prototype-builtins": "error",
"prefer-const": "warn",
// --- Medium priority ---
"eqeqeq": ["error", "always", { "null": "ignore" }],
// --- Zero-cost safety (no violations, pure prevention) ---
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
// XSS prevention - detect unescaped data in innerHTML/outerHTML/document.write
"no-unsanitized/property": ["error", escapeConfig],
"no-unsanitized/method": ["error", escapeConfig],
// --- Zero-cost safety bundle (all zero or trivial violations at time of enabling) ---
// Duplicate detection
"no-dupe-args": "error",
"no-dupe-keys": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
// Dead / unreachable code
"no-unreachable": "error",
"no-useless-catch": "error",
"no-useless-concat": "error",
"no-useless-escape": "error",
"no-empty": ["error", { "allowEmptyCatch": true }],
// Real-bug catchers
"no-cond-assign": "error",
"no-constant-condition": "error",
"no-func-assign": "error",
"no-invalid-regexp": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"use-isnan": "error",
"valid-typeof": "error",
// Code discipline
"no-debugger": "error",
"no-redeclare": "error",
"no-throw-literal": "error",
// --- Zero-cost safety bundle v2 ---
// Correctness / real-bug catchers
"no-async-promise-executor": "error",
"no-class-assign": "error",
"no-const-assign": "error",
"no-import-assign": "error",
"no-setter-return": "error",
"no-this-before-super": "error",
"no-with": "error",
"no-octal": "error",
"no-octal-escape": "error",
"default-param-last": "error",
"no-multi-assign": "error",
// Dead / pointless code
"no-extra-bind": "error",
"no-extra-semi": "error",
"no-lone-blocks": "error",
"no-sequences": "error",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-void": "error",
"no-labels": "error",
"no-unneeded-ternary": "error",
// Prefer literals / modern idioms
"no-new-object": "error",
"no-array-constructor": "error",
"prefer-numeric-literals": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-spread": "error",
"prefer-rest-params": "error",
"prefer-exponentiation-operator": "error",
// Safety discipline
"no-iterator": "error",
"no-proto": "error",
"no-extend-native": "error",
"default-case-last": "error",
// Formatting (acting as formatter — no Prettier for JS in this repo)
"no-trailing-spaces": "error",
"no-mixed-spaces-and-tabs": "error",
// --- Zero-cost safety bundle v3 ---
// Correctness / real-bug catchers
"no-implicit-globals": "error",
"no-new-native-nonconstructor": "error",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-nonoctal-decimal-escape": "error",
"no-shadow-restricted-names": "error",
"no-unmodified-loop-condition": "error",
"no-unreachable-loop": "error",
"no-useless-backreference": "error",
"no-duplicate-imports": "error",
// Prefer modern idioms
"prefer-regex-literals": "error",
"no-object-constructor": "error",
"logical-assignment-operators": ["error", "always"],
"operator-assignment": "error",
// Dead / pointless code
"no-undef-init": "error",
"no-template-curly-in-string": "error",
"no-multi-str": "error",
"no-confusing-arrow": ["error", { "allowParens": true }],
// Accessors & classes
"grouped-accessor-pairs": "error",
"no-unused-private-class-members": "error",
// Style discipline
"func-name-matching": "error",
"require-yield": "error",
"symbol-description": "error",
"yoda": "error",
// --- Zero-cost safety bundle v4 ---
// Correctness / real-bug catchers
"accessor-pairs": "error",
"getter-return": "error",
"no-dupe-class-members": "error",
"no-ex-assign": "error",
"no-fallthrough": "error",
"no-global-assign": "error",
"no-misleading-character-class": "error",
"no-regex-spaces": "error",
"no-sparse-arrays": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
// Dead / pointless code
"no-div-regex": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-extra-label": "error",
"no-inner-declarations": "error",
// Exhaustiveness
"default-case": "error",
"new-cap": ["error", { "newIsCapExceptions": ["jsPDFLib"] }],
// --- Zero-cost safety bundle v5 ---
"no-constant-binary-expression": "error",
"no-compare-neg-zero": "error",
"for-direction": "error",
"no-buffer-constructor": "error",
"no-path-concat": "error",
// Prefer ES2015+ object literal shorthand
"object-shorthand": "error",
// Catch dead stores: `let x = 1; x = 2;` where the first 1 is never read
"no-useless-assignment": "error",
// Forbid variable shadowing — clearer reads, fewer accidental rebinds
"no-shadow": "error",
// Catch `return a = b` (almost always unintentional) and lexical declarations
// floating in switch cases (which leak across cases without block scoping)
"no-return-assign": "error",
"no-case-declarations": "error",
// Always pass radix to parseInt — explicit base avoids surprise
"radix": "error",
// Readability: drop pointless `else` after `return`, fold `else { if }` into `else if`
"no-else-return": "error",
"no-lonely-if": "error",
// Catch bare expression statements like `foo.bar;` (probably meant `foo.bar()`).
// Chai-friendly variant exempts `expect(x).to.be.true` style assertions.
"no-unused-expressions": "off",
"chai-friendly/no-unused-expressions": "error",
// Force SafeLogger usage in browser code — raw console.* leaks
// sensitive data to client-side logs in production. SafeLogger
// (security/safe-logger.js) sanitises output. Tests and the
// SafeLogger module itself are exempted via overrides below.
"no-console": "error",
// Bug-detection trio:
// - consistent-return: every return path returns the same shape so
// callers don't have to special-case undefined
// - no-loop-func: closures inside a loop that capture mutable loop
// state — classic stale-reference hazard
// - require-atomic-updates: state updated based on a pre-await read
// is racy if the variable can change during the await
"consistent-return": "error",
"no-loop-func": "error",
"require-atomic-updates": "error",
},
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: {
// Browser globals
window: "readonly",
document: "readonly",
console: "readonly",
setTimeout: "readonly",
setInterval: "readonly",
clearTimeout: "readonly",
clearInterval: "readonly",
fetch: "readonly",
URL: "readonly",
URLSearchParams: "readonly",
FormData: "readonly",
Blob: "readonly",
File: "readonly",
FileReader: "readonly",
localStorage: "readonly",
sessionStorage: "readonly",
navigator: "readonly",
location: "readonly",
history: "readonly",
CustomEvent: "readonly",
Event: "readonly",
EventTarget: "readonly",
HTMLElement: "readonly",
Element: "readonly",
Node: "readonly",
NodeList: "readonly",
MutationObserver: "readonly",
ResizeObserver: "readonly",
IntersectionObserver: "readonly",
requestAnimationFrame: "readonly",
cancelAnimationFrame: "readonly",
requestIdleCallback: "readonly",
performance: "readonly",
// Browser dialogs and built-in APIs
alert: "readonly",
confirm: "readonly",
prompt: "readonly",
AbortController: "readonly",
getComputedStyle: "readonly",
XMLHttpRequest: "readonly",
Image: "readonly",
Notification: "readonly",
CSS: "readonly",
// The deprecated `event` magic global — IE-era leftover. Some inline
// template handlers still use it; mark readonly so the rule doesn't
// flag them while we phase that out.
event: "readonly",
// Project globals — all exposed via window.<name> = ... at module init.
// SafeLogger: security/safe-logger.js (sanitises console output)
// URLBuilder, URLS, URLValidator: URL helpers loaded in base.html
// ResearchStates: config/constants.js — research-status state machine
// safeFetch: utils/safe-fetch.js — fetch wrapper with URL validation
// escapeHtml, sanitizeHtml: security/xss-protection.js — XSS escaping
// LDR_CONSTANTS: config/constants.js — shared frontend constants
// DeleteManager: deletion/delete_manager.js — deletion UI helper
SafeLogger: "readonly",
URLBuilder: "readonly",
URLS: "readonly",
URLValidator: "readonly",
ResearchStates: "readonly",
safeFetch: "readonly",
escapeHtml: "readonly",
sanitizeHtml: "readonly",
LDR_CONSTANTS: "readonly",
DeleteManager: "readonly",
// Top-level helper in services/ui.js used as a fallback before
// window.escapeHtml is loaded.
escapeHtmlFallback: "readonly",
// Helpers exposed via window.X = ... in components/news.js
showModal: "readonly",
hideModal: "readonly",
showEmptyState: "readonly",
showLoadingState: "readonly",
showErrorState: "readonly",
showMessage: "readonly",
formatTimeAgo: "readonly",
createTag: "readonly",
debounce: "readonly",
// Markdown helper in services/ui.js
renderMarkdown: "readonly",
// Page-injected globals from Jinja template inline scripts
COLLECTION_ID: "readonly",
DEFAULT_LIBRARY_COLLECTION_ID: "readonly",
COLLECTIONS_DATA: "readonly",
// Third-party libs loaded via <script> tags in base.html / per-page templates
bootstrap: "readonly",
Chart: "readonly",
DOMPurify: "readonly",
marked: "readonly",
Prism: "readonly",
jsPDF: "readonly",
html2canvas: "readonly",
io: "readonly",
// Node.js globals (for config files)
module: "readonly",
require: "readonly",
process: "readonly",
__dirname: "readonly",
__filename: "readonly",
exports: "readonly",
Buffer: "readonly",
global: "readonly",
},
},
},
// SafeLogger is the canonical console.* wrapper — it MUST use raw
// console internally. Same pattern the pre-commit hook applies.
{
files: ["**/security/safe-logger.js"],
rules: {
"no-console": "off",
},
},
// Tests run in Node (Puppeteer/Playwright/vitest harnesses) where
// SafeLogger isn't loaded. Plain console.* is the right choice there.
// Matches the pre-commit hook's exclude pattern.
{
files: [
"tests/**/*.js",
"tests/**/*.mjs",
"**/*.test.js",
"**/*.spec.js",
],
rules: {
"no-console": "off",
},
languageOptions: {
globals: {
// Test framework globals (vitest / jest / mocha-style)
describe: "readonly",
it: "readonly",
test: "readonly",
expect: "readonly",
vi: "readonly",
jest: "readonly",
beforeEach: "readonly",
afterEach: "readonly",
beforeAll: "readonly",
afterAll: "readonly",
before: "readonly",
after: "readonly",
// Browser globals used inside page.evaluate(...) callbacks — they
// run in the browser context, not Node.
$: "readonly", // jQuery, when loaded on the page
KeyboardEvent: "readonly",
MouseEvent: "readonly",
Response: "readonly",
CSSRule: "readonly",
// Page-defined functions referenced from page.evaluate() with a
// `typeof X === 'function'` guard
handleTestRun: "readonly",
},
},
},
];