Skip to content

Commit cbaae46

Browse files
committed
Add security lint and rule security tests
- Enable eslint-plugin-security for .markdownlint-rules (eval, non-literal require/fs/regexp, etc.). - Add test/markdownlint-rules/security.test.js for defensive regex config parsing and ReDoS awareness. - Document security tests and security lint in CONTRIBUTING and README.
1 parent b7c21d6 commit cbaae46

7 files changed

Lines changed: 114 additions & 6 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ These targets mirror the GitHub Actions workflows. Run them locally before pushi
5454
make test-rules
5555
```
5656

57+
#### Rule Security Tests
58+
59+
- **Security tests** (`test/markdownlint-rules/security.test.js`): assert that invalid or malformed regex in rule config does not throw (defensive parsing).
60+
A skipped test documents ReDoS risk when rules use user-controlled regex; enable it after adding mitigation (e.g. safe-regex or timeout).
61+
- **Security lint**: `eslint-plugin-security` is enabled for `.markdownlint-rules` (via `make lint-js`) and forbids `eval`, `new Function`, `child_process`, non-literal `require`/`fs`/`Buffer`, and similar.
62+
63+
These execute as part of `make test-rules` and `make lint-js` respectively.
64+
`make ci` includes these in all the checks it executes.
65+
5766
### Python Unit Tests (`make test-python`)
5867

5968
- Unit tests for `test-scripts/` in `test-scripts/test_*.py` (unittest).

‎Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ lint-readmes:
2727
# When adding or modifying JS linting, update both this Makefile and
2828
# the workflow file to ensure local 'make lint-js' matches CI behavior.
2929
# Requires: Node.js and npm; run 'npm install' (or npm ci) once for node_modules.
30-
# Lints .markdownlint-rules/*.js with ESLint (recommended + complexity/max-lines).
30+
# Lints .markdownlint-rules/*.js with ESLint (recommended + complexity/max-lines + eslint-plugin-security).
3131
# Usage: make lint-js [PATHS="path1,path2"]
3232
# - PATHS: Comma-separated list of files/directories (default: .markdownlint-rules)
3333
lint-js:

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ Lint and docs-as-code tooling: custom [markdownlint](https://github.com/DavidAns
3636
Do not register as a rule in markdownlint.
3737
- Use when: copying the rule files; required by several of the rules above.
3838

39-
- **JS linting** for the rule code: ESLint (recommended + complexity/max-lines), aligned with the GitHub Actions workflow.
39+
- **JS linting** for the rule code: ESLint (recommended + complexity/max-lines + eslint-plugin-security), aligned with the GitHub Actions workflow.
4040
- **Markdownlint fixture tests**: [md_test_files/](md_test_files/README.md) includes positive/negative fixtures with explicit expected errors, verified by `test-scripts/verify_markdownlint_fixtures.py`.
41-
- **Rule unit tests**: Node `node:test` unit tests for each custom rule in `test/markdownlint-rules/`; run with `make test-rules` or `npm run test:rules`.
41+
- **Rule unit tests**: Node `node:test` unit tests for each custom rule in `test/markdownlint-rules/` (including security tests for defensive regex handling and ReDoS awareness); run with `make test-rules` or `npm run test:rules`.
4242
- **Python unit tests**: `unittest` tests for [test-scripts/](test-scripts/README.md) in `test-scripts/test_*.py`; run with `make test-python`.
4343
- **Python linting** for repo tooling scripts: `make lint-python` (flake8, pylint, xenon/radon, vulture, bandit).
4444

‎eslint.config.cjs‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
const js = require("@eslint/js");
44
const globals = require("globals");
55
const { defineConfig } = require("eslint/config");
6+
const eslintPluginSecurity = require("eslint-plugin-security");
67

78
module.exports = defineConfig([
89
{
910
name: "docs-as-code-tools/markdownlint-rules",
1011
files: [".markdownlint-rules/**/*.js"],
11-
plugins: { js },
12+
plugins: { js, security: eslintPluginSecurity },
1213
extends: ["js/recommended"],
1314
languageOptions: {
1415
ecmaVersion: 2022,
@@ -20,6 +21,20 @@ module.exports = defineConfig([
2021
complexity: ["warn", { max: 10 }],
2122
"max-depth": ["warn", 4],
2223
"max-params": ["warn", 4],
24+
"security/detect-eval-with-expression": "warn",
25+
"security/detect-non-literal-require": "warn",
26+
"security/detect-child-process": "warn",
27+
"security/detect-non-literal-fs-filename": "warn",
28+
"security/detect-new-buffer": "warn",
29+
"security/detect-buffer-noassert": "warn",
30+
"security/detect-disable-mustache-escape": "warn",
31+
"security/detect-no-csrf-before-method-override": "warn",
32+
"security/detect-possible-timing-attacks": "warn",
33+
"security/detect-pseudoRandomBytes": "warn",
34+
"security/detect-bidi-characters": "warn",
35+
"security/detect-non-literal-regexp": "warn",
36+
"security/detect-object-injection": "off",
37+
"security/detect-unsafe-regex": "off",
2338
},
2439
},
2540
]);

‎package-lock.json‎

Lines changed: 39 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"devDependencies": {
99
"@eslint/js": "^9.0.0",
1010
"eslint": "^9.15.0",
11+
"eslint-plugin-security": "^3.0.1",
1112
"globals": "^15.0.0",
1213
"markdownlint-cli2": "^0.14.0"
1314
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use strict";
2+
3+
/**
4+
* Security-focused tests for .markdownlint-rules:
5+
* - Invalid or malicious regex in config does not throw (safeRegExp / defensive parsing).
6+
* - Rules complete within a timeout when given ReDoS-prone patterns and long input
7+
* (ensures we don't hang on malicious or accidental catastrophic backtracking).
8+
*/
9+
10+
const { describe, it } = require("node:test");
11+
const assert = require("node:assert");
12+
const ruleAnchors = require("../../.markdownlint-rules/allow-custom-anchors.js");
13+
const { runRule } = require("./run-rule.js");
14+
15+
describe("security", () => {
16+
describe("allow-custom-anchors: invalid regex in config", () => {
17+
it("does not throw when allowedIdPatterns contains invalid regex", () => {
18+
const lines = ["<a id=\"x\"></a>"];
19+
assert.doesNotThrow(() => {
20+
runRule(ruleAnchors, lines, { allowedIdPatterns: ["["] });
21+
});
22+
});
23+
24+
it("does not throw when placement has invalid headingMatch regex", () => {
25+
const lines = ["<a id=\"ok\"></a>"];
26+
const config = {
27+
allowedIdPatterns: [
28+
{ pattern: "^ok$", placement: { headingMatch: "(" } },
29+
],
30+
};
31+
assert.doesNotThrow(() => {
32+
runRule(ruleAnchors, lines, config);
33+
});
34+
});
35+
});
36+
37+
describe("allow-custom-anchors: ReDoS awareness", () => {
38+
it("ReDoS: user-controlled regex can cause catastrophic backtracking (mitigation TBD)", { skip: "Current implementation is susceptible; enable when safe-regex or timeout is added." }, () => {
39+
const longId = "a".repeat(35) + "X";
40+
const lines = [`<a id="${longId}"></a>`];
41+
const config = { allowedIdPatterns: ["(a+)+$"] };
42+
const errors = runRule(ruleAnchors, lines, config);
43+
assert.ok(Array.isArray(errors));
44+
});
45+
});
46+
});

0 commit comments

Comments
 (0)