mirror of
https://github.com/LearningCircuit/local-deep-research.git
synced 2026-06-16 20:10:39 +03:00
* 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.
423 lines
15 KiB
JavaScript
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",
|
|
},
|
|
},
|
|
},
|
|
];
|