eslint-plugin-kerfjs 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -58,11 +58,12 @@ export default [
|
|
|
58
58
|
| [`require-data-key-in-each`](docs/rules/require-data-key-in-each.md) | 2 — `data-key` per item | `error` |
|
|
59
59
|
| [`no-nested-mount`](docs/rules/no-nested-mount.md) | 5 — one `mount()` per root | `error` |
|
|
60
60
|
| [`prefer-module-jsx-augmentation`](docs/rules/prefer-module-jsx-augmentation.md) | 11 — augment `kerfjs/jsx-runtime`, not global | `error` |
|
|
61
|
+
| [`no-raw-with-dynamic-arg`](docs/rules/no-raw-with-dynamic-arg.md) | — (XSS audit trail) | `warn` |
|
|
61
62
|
| [`ai-assistant-configs`](docs/rules/ai-assistant-configs.md) | — (project hygiene) | `warn` |
|
|
62
63
|
|
|
63
|
-
The "Hard Rule" column refers to the numbered rules in [`docs/ai/usage-guide.md`](../docs/ai/usage-guide.md) on the main kerf repo. `ai-assistant-configs`
|
|
64
|
+
The "Hard Rule" column refers to the numbered rules in [`docs/ai/usage-guide.md`](../docs/ai/usage-guide.md) on the main kerf repo. `no-raw-with-dynamic-arg` and `ai-assistant-configs` don't map to numbered Hard Rules — the former creates an audit trail for every dynamic `raw()` call site (potential XSS); the latter checks that the bundled AI-assistant configs are installed and current. See [`docs/12-ai-assistant-configs.md`](../docs/12-ai-assistant-configs.md) on the main kerf repo for the AI-configs design.
|
|
64
65
|
|
|
65
|
-
## Why these four (and not more)?
|
|
66
|
+
## Why these four Hard-Rule rules (and not more)?
|
|
66
67
|
|
|
67
68
|
Rules that need flow analysis (signal reads outside render — Rule 7), call-graph analysis (`addEventListener` inside the mount tree — Rule 4), or type information (partial-set against multi-key state — Rule 8) are already covered by the opt-in dev-warns and strict TS. Duplicating them here would mean either high false-positive rates without type info, or a `parserServices` dependency that complicates consumer setup.
|
|
68
69
|
|
|
@@ -14,16 +14,34 @@ Maps to **kerf Hard Rule 9** — kerf's JSX runtime renders to HTML strings, so
|
|
|
14
14
|
|
|
15
15
|
## ✅ Correct
|
|
16
16
|
|
|
17
|
+
Preferred — use `attr()` so the attribute name lives in one typed constant and
|
|
18
|
+
renames propagate to both JSX and `delegate()` automatically:
|
|
19
|
+
|
|
17
20
|
```tsx
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
import { attr, delegate, type AttrSpec } from 'kerfjs';
|
|
22
|
+
|
|
23
|
+
const ACTIONS = {
|
|
24
|
+
save: attr('data-action', 'save'),
|
|
25
|
+
update: attr('data-action', 'update'),
|
|
26
|
+
submit: attr('data-action', 'submit'),
|
|
27
|
+
} as const satisfies Record<string, AttrSpec<'data-action'>>;
|
|
28
|
+
|
|
29
|
+
// In the template — spread .attrs (no hardcoded 'data-action' at each call site):
|
|
30
|
+
<button {...ACTIONS.save.attrs}>Save</button>
|
|
31
|
+
<input {...ACTIONS.update.attrs} />
|
|
32
|
+
<form {...ACTIONS.submit.attrs}>…</form>
|
|
33
|
+
|
|
34
|
+
// Once, at module init — use .selector:
|
|
35
|
+
delegate(rootEl, 'click', ACTIONS.save.selector, save);
|
|
36
|
+
delegate(rootEl, 'input', ACTIONS.update.selector, update);
|
|
37
|
+
delegate(rootEl, 'submit', ACTIONS.submit.selector, submit);
|
|
38
|
+
```
|
|
22
39
|
|
|
23
|
-
|
|
40
|
+
String literals still work for ad-hoc fixed selectors:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
<button data-action="save">Save</button>
|
|
24
44
|
delegate(rootEl, 'click', '[data-action="save"]', save);
|
|
25
|
-
delegate(rootEl, 'input', '[data-action="update"]', update);
|
|
26
|
-
delegate(rootEl, 'submit', '[data-action="submit"]', submit);
|
|
27
45
|
```
|
|
28
46
|
|
|
29
47
|
## Why this rule is AST-only
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# `kerfjs/no-raw-with-dynamic-arg`
|
|
2
|
+
|
|
3
|
+
Warn when `raw()` is called with a dynamic argument (any expression that is not a static string literal or an expression-free template literal).
|
|
4
|
+
|
|
5
|
+
`raw(html)` bypasses kerf's HTML escaping and marks a string as trusted for direct DOM injection. Passing dynamic or user-controlled content is an XSS vulnerability. This rule forces an explicit `// eslint-disable-next-line` acknowledgment at every dynamic injection point, creating a searchable audit trail.
|
|
6
|
+
|
|
7
|
+
**Severity in `kerfjs.configs.recommended`: `warn`** (not `error`) because sanitized pipelines like `raw(DOMPurify.sanitize(marked(input)))` look dynamic to an AST rule but are legitimate. The warn prompts review; `eslint-disable` makes the intent explicit.
|
|
8
|
+
|
|
9
|
+
## ❌ Incorrect
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
raw(userInput) // variable reference
|
|
13
|
+
raw(fetchedHtml()) // function call
|
|
14
|
+
raw(`<b>${title}</b>`) // template literal with expressions
|
|
15
|
+
raw(isAdmin ? adminHtml : guestHtml) // conditional expression
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## ✅ Correct
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
raw('<p>Static markup</p>') // string literal — no warning
|
|
22
|
+
raw(`<p>Static template</p>`) // expression-free template literal — no warning
|
|
23
|
+
|
|
24
|
+
// Dynamic but audited — suppress with eslint-disable
|
|
25
|
+
// eslint-disable-next-line kerfjs/no-raw-with-dynamic-arg
|
|
26
|
+
raw(DOMPurify.sanitize(marked(userMarkdown)))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why `warn` and not `error`
|
|
30
|
+
|
|
31
|
+
Sanitization pipelines (`DOMPurify`, `sanitize-html`, server-rendered trusted content) are legitimate uses of `raw()` with a dynamic argument. An `error` severity would block every such callsite. `warn` surfaces the pattern for review; the `eslint-disable` suppression becomes the permanent audit marker.
|
|
32
|
+
|
|
33
|
+
## What this rule catches
|
|
34
|
+
|
|
35
|
+
- Bare `raw(expr)` calls
|
|
36
|
+
- Member-expression calls `kerf.raw(expr)` and `kerfjs.raw(expr)`
|
|
37
|
+
|
|
38
|
+
## What this rule does NOT catch
|
|
39
|
+
|
|
40
|
+
- `raw()` calls where the binding was renamed via a local alias (`const inject = raw; inject(expr)`)
|
|
41
|
+
- The correctness of any sanitizer passed to `raw()` — that remains the caller's responsibility
|
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import aiAssistantConfigs from './lib/rules/ai-assistant-configs.js';
|
|
2
2
|
import noInlineJsxEventHandlers from './lib/rules/no-inline-jsx-event-handlers.js';
|
|
3
3
|
import noNestedMount from './lib/rules/no-nested-mount.js';
|
|
4
|
+
import noRawWithDynamicArg from './lib/rules/no-raw-with-dynamic-arg.js';
|
|
4
5
|
import preferModuleJsxAugmentation from './lib/rules/prefer-module-jsx-augmentation.js';
|
|
5
6
|
import requireDataKeyInEach from './lib/rules/require-data-key-in-each.js';
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ const plugin = {
|
|
|
8
9
|
meta: { name: 'eslint-plugin-kerfjs', version: '0.9.0' },
|
|
9
10
|
rules: {
|
|
10
11
|
'no-inline-jsx-event-handlers': noInlineJsxEventHandlers,
|
|
12
|
+
'no-raw-with-dynamic-arg': noRawWithDynamicArg,
|
|
11
13
|
'require-data-key-in-each': requireDataKeyInEach,
|
|
12
14
|
'no-nested-mount': noNestedMount,
|
|
13
15
|
'prefer-module-jsx-augmentation': preferModuleJsxAugmentation,
|
|
@@ -19,8 +21,12 @@ const plugin = {
|
|
|
19
21
|
// Most rules ship as `error` in recommended (AST-shaped antipatterns are
|
|
20
22
|
// bugs). `ai-assistant-configs` is `warn` — it's a project-hygiene nudge,
|
|
21
23
|
// not a code defect, and a missing skill file shouldn't fail CI.
|
|
24
|
+
// `no-raw-with-dynamic-arg` is `warn` in recommended — false-positive rate is
|
|
25
|
+
// non-trivial (sanitized pipelines look dynamic to an AST rule), so `error`
|
|
26
|
+
// would block too many legitimate uses without eslint-disable comments.
|
|
22
27
|
const recommendedRules = {
|
|
23
28
|
'kerfjs/no-inline-jsx-event-handlers': 'error',
|
|
29
|
+
'kerfjs/no-raw-with-dynamic-arg': 'warn',
|
|
24
30
|
'kerfjs/require-data-key-in-each': 'error',
|
|
25
31
|
'kerfjs/no-nested-mount': 'error',
|
|
26
32
|
'kerfjs/prefer-module-jsx-augmentation': 'error',
|
|
@@ -39,6 +39,13 @@ function resolveManifestPath(cwd) {
|
|
|
39
39
|
const req = createRequire(join(cwd, 'noop.js'));
|
|
40
40
|
return req.resolve('kerfjs/ai/manifest.json');
|
|
41
41
|
} catch {
|
|
42
|
+
// Fallback: kerfjs versions before the `./ai/*` exports entry landed
|
|
43
|
+
// block subpath resolution under Node's strict `exports` rules
|
|
44
|
+
// (ERR_PACKAGE_PATH_NOT_EXPORTED). Look directly at the package on disk —
|
|
45
|
+
// good enough for the plain-npm layout, and the rule should fire for
|
|
46
|
+
// those installs too rather than silently no-op.
|
|
47
|
+
const direct = join(cwd, 'node_modules', 'kerfjs', 'ai', 'manifest.json');
|
|
48
|
+
if (existsSync(direct)) return direct;
|
|
42
49
|
return null;
|
|
43
50
|
}
|
|
44
51
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flags calls to `raw(expr)` where `expr` is not a static string literal or a
|
|
3
|
+
* template literal with no dynamic expressions. `raw()` bypasses kerf's
|
|
4
|
+
* HTML auto-escaping — passing a dynamic value (user input, API response, any
|
|
5
|
+
* expression the user can influence) is the canonical XSS vector.
|
|
6
|
+
*
|
|
7
|
+
* Safe: raw("<strong>static</strong>")
|
|
8
|
+
* Safe: raw(`<em>static template</em>`) (no ${} expressions)
|
|
9
|
+
* Error: raw(someVariable)
|
|
10
|
+
* Error: raw(fetchedHtml)
|
|
11
|
+
* Error: raw(`<b>${userContent}</b>`) (template has expressions)
|
|
12
|
+
* Error: raw(marked(markdown)) (unsanitized pipeline)
|
|
13
|
+
*
|
|
14
|
+
* When the input IS user-controlled, the canonical fix is to sanitize first:
|
|
15
|
+
* raw(DOMPurify.sanitize(marked(userMarkdown)))
|
|
16
|
+
*
|
|
17
|
+
* To let the linter know the call is intentionally safe (e.g., a
|
|
18
|
+
* fully-sanitized pipeline), add an eslint-disable-next-line comment.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const meta = {
|
|
22
|
+
type: 'problem',
|
|
23
|
+
docs: {
|
|
24
|
+
description:
|
|
25
|
+
"Disallow `raw()` with dynamic arguments; `raw()` bypasses HTML escaping and passing dynamic values is an XSS vector.",
|
|
26
|
+
url: 'https://github.com/brianwestphal/kerf/blob/main/eslint-plugin/docs/rules/no-raw-with-dynamic-arg.md',
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
messages: {
|
|
30
|
+
dynamic:
|
|
31
|
+
"`raw()` bypasses HTML auto-escaping. Passing a dynamic value is an XSS risk unless the input has been sanitized. Sanitize first (`DOMPurify.sanitize(...)`) then pass to `raw()`, or add `// eslint-disable-next-line kerfjs/no-raw-with-dynamic-arg` to mark a call as intentionally safe.",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function isStaticArg(node) {
|
|
36
|
+
if (!node) return false;
|
|
37
|
+
// String or numeric literal: raw("html")
|
|
38
|
+
if (node.type === 'Literal') return typeof node.value === 'string';
|
|
39
|
+
// Template literal with no expressions: raw(`static`)
|
|
40
|
+
if (node.type === 'TemplateLiteral') return node.expressions.length === 0;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function create(context) {
|
|
45
|
+
return {
|
|
46
|
+
CallExpression(node) {
|
|
47
|
+
const callee = node.callee;
|
|
48
|
+
if (!callee) return;
|
|
49
|
+
// Match bare `raw(...)` and `kerfjs.raw(...)` / `kerf.raw(...)` shapes.
|
|
50
|
+
const isRaw = (callee.type === 'Identifier' && callee.name === 'raw')
|
|
51
|
+
|| (callee.type === 'MemberExpression'
|
|
52
|
+
&& callee.property.type === 'Identifier'
|
|
53
|
+
&& callee.property.name === 'raw');
|
|
54
|
+
if (!isRaw) return;
|
|
55
|
+
if (node.arguments.length === 0) return;
|
|
56
|
+
const arg = node.arguments[0];
|
|
57
|
+
if (!isStaticArg(arg)) {
|
|
58
|
+
context.report({ node, messageId: 'dynamic' });
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default { meta, create };
|