eslint-plugin-jwt-security 2.3.0 → 2.3.2
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 +5 -5
- package/package.json +16 -24
- package/src/index.d.ts +17 -0
- package/src/index.js +1 -0
- package/src/oxlint.d.ts +17 -0
- package/src/oxlint.js +1 -0
- package/src/rules/no-algorithm-confusion/index.js +1 -0
- package/src/rules/no-algorithm-none/index.js +1 -0
- package/src/rules/no-decode-without-verify/index.js +1 -0
- package/src/rules/no-hardcoded-secret/index.js +1 -0
- package/src/rules/no-sensitive-payload/index.js +1 -0
- package/src/rules/no-timestamp-manipulation/index.js +1 -0
- package/src/rules/no-weak-secret/index.js +1 -0
- package/src/rules/require-algorithm-whitelist/index.js +1 -0
- package/src/rules/require-audience-validation/index.js +1 -0
- package/src/rules/require-expiration/index.js +1 -0
- package/src/rules/require-issued-at/index.js +1 -0
- package/src/rules/require-issuer-validation/index.js +1 -0
- package/src/rules/require-max-age/index.js +1 -0
- package/src/types/index.js +1 -0
- package/src/utils/index.js +1 -0
- package/AGENTS.md +0 -102
- package/CHANGELOG.md +0 -161
- package/dist/AGENTS.md +0 -102
- package/dist/CHANGELOG.md +0 -161
- package/dist/LICENSE +0 -23
- package/dist/README.md +0 -158
- package/dist/package.json +0 -125
- package/dist/src/index.d.ts +0 -101
- package/dist/src/index.js +0 -171
- package/dist/src/oxlint.d.ts +0 -20
- package/dist/src/oxlint.js +0 -21
- package/dist/src/rules/no-algorithm-confusion/index.d.ts +0 -24
- package/dist/src/rules/no-algorithm-confusion/index.js +0 -165
- package/dist/src/rules/no-algorithm-none/index.d.ts +0 -24
- package/dist/src/rules/no-algorithm-none/index.js +0 -222
- package/dist/src/rules/no-decode-without-verify/index.d.ts +0 -23
- package/dist/src/rules/no-decode-without-verify/index.js +0 -131
- package/dist/src/rules/no-hardcoded-secret/index.d.ts +0 -23
- package/dist/src/rules/no-hardcoded-secret/index.js +0 -189
- package/dist/src/rules/no-sensitive-payload/index.d.ts +0 -20
- package/dist/src/rules/no-sensitive-payload/index.js +0 -103
- package/dist/src/rules/no-timestamp-manipulation/index.d.ts +0 -23
- package/dist/src/rules/no-timestamp-manipulation/index.js +0 -97
- package/dist/src/rules/no-weak-secret/index.d.ts +0 -23
- package/dist/src/rules/no-weak-secret/index.js +0 -165
- package/dist/src/rules/require-algorithm-whitelist/index.d.ts +0 -24
- package/dist/src/rules/require-algorithm-whitelist/index.js +0 -113
- package/dist/src/rules/require-audience-validation/index.d.ts +0 -20
- package/dist/src/rules/require-audience-validation/index.js +0 -100
- package/dist/src/rules/require-expiration/index.d.ts +0 -23
- package/dist/src/rules/require-expiration/index.js +0 -136
- package/dist/src/rules/require-issued-at/index.d.ts +0 -23
- package/dist/src/rules/require-issued-at/index.js +0 -113
- package/dist/src/rules/require-issuer-validation/index.d.ts +0 -20
- package/dist/src/rules/require-issuer-validation/index.js +0 -100
- package/dist/src/rules/require-max-age/index.d.ts +0 -24
- package/dist/src/rules/require-max-age/index.js +0 -93
- package/dist/src/types/index.js +0 -7
- package/dist/src/utils/index.d.ts +0 -98
- package/dist/src/utils/index.js +0 -238
- package/src/__compatibility__/jwt-interface.spec.ts +0 -192
- package/src/index.test.ts +0 -47
- package/src/index.ts +0 -209
- package/src/oxlint.test.ts +0 -61
- package/src/oxlint.ts +0 -23
- package/src/rules/no-algorithm-confusion/index.ts +0 -208
- package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +0 -106
- package/src/rules/no-algorithm-none/index.ts +0 -270
- package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +0 -146
- package/src/rules/no-decode-without-verify/index.ts +0 -162
- package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +0 -76
- package/src/rules/no-hardcoded-secret/index.ts +0 -230
- package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +0 -97
- package/src/rules/no-sensitive-payload/index.ts +0 -123
- package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +0 -157
- package/src/rules/no-timestamp-manipulation/index.ts +0 -118
- package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +0 -54
- package/src/rules/no-weak-secret/index.ts +0 -204
- package/src/rules/no-weak-secret/no-weak-secret.test.ts +0 -114
- package/src/rules/require-algorithm-whitelist/index.ts +0 -145
- package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +0 -72
- package/src/rules/require-audience-validation/index.ts +0 -120
- package/src/rules/require-audience-validation/require-audience-validation.test.ts +0 -68
- package/src/rules/require-expiration/index.ts +0 -170
- package/src/rules/require-expiration/require-expiration.test.ts +0 -135
- package/src/rules/require-issued-at/index.ts +0 -143
- package/src/rules/require-issued-at/require-issued-at.test.ts +0 -64
- package/src/rules/require-issuer-validation/index.ts +0 -120
- package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +0 -68
- package/src/rules/require-max-age/index.ts +0 -117
- package/src/rules/require-max-age/require-max-age.test.ts +0 -69
- package/src/tests/coverage-dual-layer.test.ts +0 -323
- package/src/types/index.ts +0 -107
- package/src/utils/index.ts +0 -285
- package/src/utils/utils.test.ts +0 -324
- /package/{dist/src → src}/types/index.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-jwt-security" target="blank"><img src="https://eslint.interlace.tools/
|
|
2
|
+
<a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-jwt-security" target="blank"><img src="https://eslint.interlace.tools/logos/interlace.svg" alt="Interlace" height="90" /></a>
|
|
3
3
|
|
|
4
|
-
<a href="https://
|
|
4
|
+
<a href="https://jwt.io" target="_blank"><img src="https://eslint.interlace.tools/logos/jwt.svg" alt="JSON Web Tokens" height="90" /></a>
|
|
5
5
|
|
|
6
|
-
<a href="https://oxc.rs" target="_blank"><img src="https://eslint.interlace.tools/oxlint
|
|
6
|
+
<a href="https://oxc.rs" target="_blank"><img src="https://eslint.interlace.tools/logos/oxlint.svg" alt="oxlint" height="90" /></a>
|
|
7
7
|
|
|
8
|
-
<a href="https://
|
|
8
|
+
<a href="https://eslint.org" target="_blank"><img src="https://eslint.interlace.tools/logos/eslint.svg" alt="ESLint" height="90" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -150,7 +150,7 @@ If this plugin caught a real bug for you, **[star the repo](https://github.com/o
|
|
|
150
150
|
MIT © [Ofri Peretz](https://github.com/ofri-peretz)
|
|
151
151
|
|
|
152
152
|
<p align="center">
|
|
153
|
-
<a href="https://eslint.interlace.tools/docs/security/plugin-jwt?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-jwt-security"><img src="https://eslint.interlace.tools/images/og-jwt.png" alt="ESLint Interlace Plugin" width="100%" /></a>
|
|
153
|
+
<a href="https://eslint.interlace.tools/docs/security/plugin-jwt?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-jwt-security"><img src="https://eslint.interlace.tools/images/og-jwt-security.png" alt="ESLint Interlace Plugin" width="100%" /></a>
|
|
154
154
|
</p>
|
|
155
155
|
|
|
156
156
|
<p align="center">
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-jwt-security",
|
|
3
|
-
"version": "2.3.
|
|
4
|
-
"description": "ESLint plugin for JWT security
|
|
3
|
+
"version": "2.3.2",
|
|
4
|
+
"description": "ESLint plugin for JWT security — detects algorithm confusion (CVE-2022-23540), alg:none, weak or hardcoded secrets, and decode-without-verify.",
|
|
5
5
|
"type": "commonjs",
|
|
6
|
-
"main": "./
|
|
7
|
-
"types": "./
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./
|
|
11
|
-
"default": "./
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./types": {
|
|
14
14
|
"types": "./src/types/index.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"homepage": "https://eslint.interlace.tools/docs/security/plugin-jwt-security?utm_source=npm&utm_medium=referral&utm_campaign=eslint-plugin-jwt-security-security",
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
27
|
-
"url": "https://github.com/ofri-peretz/eslint",
|
|
27
|
+
"url": "git+https://github.com/ofri-peretz/eslint.git",
|
|
28
28
|
"directory": "packages/eslint-plugin-jwt-security"
|
|
29
29
|
},
|
|
30
30
|
"bugs": {
|
|
@@ -35,11 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"src/",
|
|
38
|
-
"dist/",
|
|
39
38
|
"README.md",
|
|
40
|
-
"LICENSE"
|
|
41
|
-
"CHANGELOG.md",
|
|
42
|
-
"AGENTS.md"
|
|
39
|
+
"LICENSE"
|
|
43
40
|
],
|
|
44
41
|
"keywords": [
|
|
45
42
|
"eslint",
|
|
@@ -82,11 +79,16 @@
|
|
|
82
79
|
"node": ">=18.0.0"
|
|
83
80
|
},
|
|
84
81
|
"dependencies": {
|
|
85
|
-
"
|
|
86
|
-
"@interlace/eslint-devkit": "^1.4.3"
|
|
82
|
+
"@interlace/eslint-devkit": "^1.10.0"
|
|
87
83
|
},
|
|
88
84
|
"peerDependencies": {
|
|
89
|
-
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
|
85
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
86
|
+
"jsonwebtoken": "^8.0.0 || ^9.0.0",
|
|
87
|
+
"@nestjs/jwt": "^9.0.0 || ^10.0.0 || ^11.0.0",
|
|
88
|
+
"express-jwt": "^7.0.0 || ^8.0.0",
|
|
89
|
+
"jose": "^4.0.0 || ^5.0.0 || ^6.0.0",
|
|
90
|
+
"jwks-rsa": "^3.0.0 || ^4.0.0",
|
|
91
|
+
"jwt-decode": "^3.0.0 || ^4.0.0"
|
|
90
92
|
},
|
|
91
93
|
"peerDependenciesMeta": {
|
|
92
94
|
"jsonwebtoken": {
|
|
@@ -108,16 +110,6 @@
|
|
|
108
110
|
"optional": true
|
|
109
111
|
}
|
|
110
112
|
},
|
|
111
|
-
"devDependencies": {
|
|
112
|
-
"@typescript-eslint/parser": "^8.46.2",
|
|
113
|
-
"@typescript-eslint/rule-tester": "^8.46.2"
|
|
114
|
-
},
|
|
115
|
-
"scripts": {
|
|
116
|
-
"build": "tsx ../../scripts/build-package.ts",
|
|
117
|
-
"test": "vitest run",
|
|
118
|
-
"test:coverage": "vitest run --coverage",
|
|
119
|
-
"typecheck": "tsc -p tsconfig.lib.json --noEmit"
|
|
120
|
-
},
|
|
121
113
|
"funding": {
|
|
122
114
|
"type": "github",
|
|
123
115
|
"url": "https://github.com/ofri-peretz/eslint"
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Minimal declaration written by scripts/build-package.ts.
|
|
2
|
+
//
|
|
3
|
+
// tsc's generated entry declaration inlined every inferred rule-option type
|
|
4
|
+
// (up to 166 kB per plugin) for an API no consumer calls directly. This types
|
|
5
|
+
// what a flat config actually touches. Per-rule option types remain available
|
|
6
|
+
// from the "./types" subpath export where a plugin provides one.
|
|
7
|
+
import type { Linter, Rule } from 'eslint';
|
|
8
|
+
|
|
9
|
+
export declare const rules: Record<string, Rule.RuleModule>;
|
|
10
|
+
export declare const configs: Record<string, Linter.Config>;
|
|
11
|
+
export declare const plugin: {
|
|
12
|
+
meta: { name: string; version: string };
|
|
13
|
+
rules: Record<string, Rule.RuleModule>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare const _default: typeof plugin;
|
|
17
|
+
export default _default;
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSensitivePayload=exports.requireMaxAge=exports.noTimestampManipulation=exports.requireAudienceValidation=exports.requireIssuerValidation=exports.requireIssuedAt=exports.noHardcodedSecret=exports.requireExpiration=exports.noWeakSecret=exports.noDecodeWithoutVerify=exports.requireAlgorithmWhitelist=exports.noAlgorithmConfusion=exports.noAlgorithmNone=exports.configs=exports.plugin=exports.rules=void 0;const no_algorithm_none_1=require("./rules/no-algorithm-none");Object.defineProperty(exports,"noAlgorithmNone",{enumerable:true,get:function(){return no_algorithm_none_1.noAlgorithmNone}});const no_algorithm_confusion_1=require("./rules/no-algorithm-confusion");Object.defineProperty(exports,"noAlgorithmConfusion",{enumerable:true,get:function(){return no_algorithm_confusion_1.noAlgorithmConfusion}});const require_algorithm_whitelist_1=require("./rules/require-algorithm-whitelist");Object.defineProperty(exports,"requireAlgorithmWhitelist",{enumerable:true,get:function(){return require_algorithm_whitelist_1.requireAlgorithmWhitelist}});const no_decode_without_verify_1=require("./rules/no-decode-without-verify");Object.defineProperty(exports,"noDecodeWithoutVerify",{enumerable:true,get:function(){return no_decode_without_verify_1.noDecodeWithoutVerify}});const no_weak_secret_1=require("./rules/no-weak-secret");Object.defineProperty(exports,"noWeakSecret",{enumerable:true,get:function(){return no_weak_secret_1.noWeakSecret}});const require_expiration_1=require("./rules/require-expiration");Object.defineProperty(exports,"requireExpiration",{enumerable:true,get:function(){return require_expiration_1.requireExpiration}});const no_hardcoded_secret_1=require("./rules/no-hardcoded-secret");Object.defineProperty(exports,"noHardcodedSecret",{enumerable:true,get:function(){return no_hardcoded_secret_1.noHardcodedSecret}});const require_issued_at_1=require("./rules/require-issued-at");Object.defineProperty(exports,"requireIssuedAt",{enumerable:true,get:function(){return require_issued_at_1.requireIssuedAt}});const require_issuer_validation_1=require("./rules/require-issuer-validation");Object.defineProperty(exports,"requireIssuerValidation",{enumerable:true,get:function(){return require_issuer_validation_1.requireIssuerValidation}});const require_audience_validation_1=require("./rules/require-audience-validation");Object.defineProperty(exports,"requireAudienceValidation",{enumerable:true,get:function(){return require_audience_validation_1.requireAudienceValidation}});const no_timestamp_manipulation_1=require("./rules/no-timestamp-manipulation");Object.defineProperty(exports,"noTimestampManipulation",{enumerable:true,get:function(){return no_timestamp_manipulation_1.noTimestampManipulation}});const require_max_age_1=require("./rules/require-max-age");Object.defineProperty(exports,"requireMaxAge",{enumerable:true,get:function(){return require_max_age_1.requireMaxAge}});const no_sensitive_payload_1=require("./rules/no-sensitive-payload");Object.defineProperty(exports,"noSensitivePayload",{enumerable:true,get:function(){return no_sensitive_payload_1.noSensitivePayload}});exports.rules={get"no-algorithm-none"(){return require("./rules/no-algorithm-none").noAlgorithmNone},get"no-algorithm-confusion"(){return require("./rules/no-algorithm-confusion").noAlgorithmConfusion},get"require-algorithm-whitelist"(){return require("./rules/require-algorithm-whitelist").requireAlgorithmWhitelist},get"no-decode-without-verify"(){return require("./rules/no-decode-without-verify").noDecodeWithoutVerify},get"no-weak-secret"(){return require("./rules/no-weak-secret").noWeakSecret},get"require-expiration"(){return require("./rules/require-expiration").requireExpiration},get"no-hardcoded-secret"(){return require("./rules/no-hardcoded-secret").noHardcodedSecret},get"require-issued-at"(){return require("./rules/require-issued-at").requireIssuedAt},get"require-issuer-validation"(){return require("./rules/require-issuer-validation").requireIssuerValidation},get"require-audience-validation"(){return require("./rules/require-audience-validation").requireAudienceValidation},get"no-timestamp-manipulation"(){return require("./rules/no-timestamp-manipulation").noTimestampManipulation},get"require-max-age"(){return require("./rules/require-max-age").requireMaxAge},get"no-sensitive-payload"(){return require("./rules/no-sensitive-payload").noSensitivePayload}};exports.plugin={meta:{name:"eslint-plugin-jwt-security",version:"2.3.2"},rules:exports.rules};const recommendedRules={"jwt/no-algorithm-none":"error","jwt/no-algorithm-confusion":"error","jwt/require-algorithm-whitelist":"error","jwt/no-decode-without-verify":"error","jwt/no-weak-secret":"error","jwt/no-hardcoded-secret":"error","jwt/no-timestamp-manipulation":"error","jwt/require-expiration":"error","jwt/no-sensitive-payload":"warn"};const strictRules={"jwt/no-algorithm-none":"error","jwt/no-algorithm-confusion":"error","jwt/require-algorithm-whitelist":"error","jwt/no-decode-without-verify":"error","jwt/no-weak-secret":"error","jwt/no-hardcoded-secret":"error","jwt/require-expiration":"error","jwt/require-issued-at":"error","jwt/require-issuer-validation":"error","jwt/require-audience-validation":"error","jwt/no-timestamp-manipulation":"error","jwt/require-max-age":"error","jwt/no-sensitive-payload":"error"};const legacyRules={"jwt/no-algorithm-none":"error","jwt/no-algorithm-confusion":"error","jwt/no-hardcoded-secret":"warn"};exports.configs={flagship:{plugins:{jwt:exports.plugin},rules:{"jwt/no-algorithm-none":"error"}},recommended:{plugins:{jwt:exports.plugin},rules:recommendedRules},strict:{plugins:{jwt:exports.plugin},rules:strictRules},legacy:{plugins:{jwt:exports.plugin},rules:legacyRules},all:{plugins:{jwt:exports.plugin},rules:strictRules}};exports.default=exports.plugin;
|
package/src/oxlint.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Minimal declaration written by scripts/build-package.ts.
|
|
2
|
+
//
|
|
3
|
+
// tsc's generated entry declaration inlined every inferred rule-option type
|
|
4
|
+
// (up to 166 kB per plugin) for an API no consumer calls directly. This types
|
|
5
|
+
// what a flat config actually touches. Per-rule option types remain available
|
|
6
|
+
// from the "./types" subpath export where a plugin provides one.
|
|
7
|
+
import type { Linter, Rule } from 'eslint';
|
|
8
|
+
|
|
9
|
+
export declare const rules: Record<string, Rule.RuleModule>;
|
|
10
|
+
export declare const configs: Record<string, Linter.Config>;
|
|
11
|
+
export declare const plugin: {
|
|
12
|
+
meta: { name: string; version: string };
|
|
13
|
+
rules: Record<string, Rule.RuleModule>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare const _default: typeof plugin;
|
|
17
|
+
export default _default;
|
package/src/oxlint.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const index_1=require("./index");module.exports=index_1.plugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noAlgorithmConfusion=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");const PUBLIC_KEY_PATTERNS=[/public/i,/\.pub$/,/\.pem$/,/-----BEGIN PUBLIC KEY-----/,/-----BEGIN RSA PUBLIC KEY-----/,/-----BEGIN EC PUBLIC KEY-----/,/getPublicKey/i,/publicKey/i,/jwks/i];exports.noAlgorithmConfusion=(0,eslint_devkit_1.createRule)({name:"no-algorithm-confusion",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-algorithm-confusion.md",description:"Prevent algorithm confusion attacks using symmetric algorithms with asymmetric keys",cwe:"CWE-347",cvss:9.5},fixable:void 0,hasSuggestions:false,messages:{algorithmConfusion:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"JWT Algorithm Confusion Attack",cwe:"CWE-347",description:"Symmetric algorithm (HS*) used with public key allows token forgery",severity:"CRITICAL",fix:"Use asymmetric algorithms (RS256, ES256) with public keys",documentationLink:"https://portswigger.net/web-security/jwt/algorithm-confusion"}),symmetricWithPublicKey:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Symmetric Algorithm with Public Key",cwe:"CWE-347",description:"HS256/384/512 should only be used with shared secrets, not public keys",severity:"CRITICAL",fix:"Switch to RS256 or ES256 for asymmetric key verification",documentationLink:"https://portswigger.net/web-security/jwt/algorithm-confusion"}),useAsymmetricAlgorithm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Asymmetric Algorithm",description:"Replace HS* with RS* or ES* algorithm",severity:"LOW",fix:"Use RS256 or ES256 with public key verification",documentationLink:"https://tools.ietf.org/html/rfc8725"})},schema:[{type:"object",properties:{symmetricAlgorithms:{type:"array",items:{type:"string"},default:["HS256","HS384","HS512"],description:"Algorithms to flag when used with public keys"},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{symmetricAlgorithms:["HS256","HS384","HS512"],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const options=context.options[0]??{};const symmetricAlgSet=new Set(options.symmetricAlgorithms??["HS256","HS384","HS512"]);const sourceCode=context.sourceCode;const looksLikePublicKey=node=>{const text=sourceCode.getText(node);return PUBLIC_KEY_PATTERNS.some(pattern=>pattern.test(text))};const hasSymmetricAlgorithm=algorithms=>{for(const alg of algorithms){if(symmetricAlgSet.has(alg)){return alg}}return null};return{CallExpression(node){if(!(0,utils_1.isVerifyOperation)(node)){return}if(node.arguments.length<2){return}const keyArg=node.arguments[1];const optionsArg=(0,utils_1.getOptionsArgument)(node,2);if(!looksLikePublicKey(keyArg)){return}if(optionsArg){const algorithms=(0,utils_1.extractAlgorithms)(optionsArg);const symmetricAlg=hasSymmetricAlgorithm(algorithms);if(symmetricAlg){for(const prop of optionsArg.properties){if(prop.type==="Property"&&prop.key.type==="Identifier"&&["algorithms","algorithm","alg"].includes(prop.key.name)){context.report({node:prop.value,messageId:"algorithmConfusion",data:{algorithm:symmetricAlg}});return}}}}}}}});exports.default=exports.noAlgorithmConfusion;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noAlgorithmNone=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.noAlgorithmNone=(0,eslint_devkit_1.createRule)({name:"no-algorithm-none",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-algorithm-none.md",description:'Disallow JWT "none" algorithm which bypasses signature verification (CVE-2022-23540)',cwe:"CWE-347",cvss:9.5,confidence:"high"},fixable:void 0,hasSuggestions:false,messages:{algorithmNone:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"JWT Algorithm None Attack",cwe:"CWE-347",description:'Using alg:"none" bypasses signature verification, allowing token forgery',severity:"CRITICAL",fix:'Remove "none" and use RS256, ES256, or other secure algorithms',documentationLink:"https://nvd.nist.gov/vuln/detail/CVE-2022-23540"}),algorithmNoneInArray:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"JWT Algorithm None in Whitelist",cwe:"CWE-347",description:'Including "none" in algorithms array allows unsigned tokens',severity:"CRITICAL",fix:'Remove "none" from the algorithms array',documentationLink:"https://nvd.nist.gov/vuln/detail/CVE-2022-23540"}),emptyAlgorithms:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Empty Algorithms Array",cwe:"CWE-347",description:"Empty algorithms array may default to accepting any algorithm including none",severity:"HIGH",fix:'Specify explicit algorithms: ["RS256"] or ["ES256"]',documentationLink:"https://tools.ietf.org/html/rfc8725"}),useSecureAlgorithm:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Secure Algorithm",description:"Replace with a secure algorithm",severity:"LOW",fix:'Use algorithms: ["RS256"] or algorithms: ["ES256"]',documentationLink:"https://tools.ietf.org/html/rfc8725"}),decodeWithoutVerify:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"JWT Decode Without Verify",cwe:"CWE-347",description:'jwt.decode() does NOT verify the signature \u2014 using its result as if it were authenticated is functionally equivalent to algorithm:"none". Any token, including forged ones, will decode.',severity:"CRITICAL",fix:'Use jwt.verify(token, secret, { algorithms: ["RS256"] }) instead. Reserve jwt.decode() for non-security inspection only (e.g. logging the issuer).',documentationLink:"https://github.com/auth0/node-jsonwebtoken#jwtdecodetoken--options"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",default:false,description:'Allow "none" algorithm in test files'},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{allowInTests:false,trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const options=context.options[0]??{};const{allowInTests=false}=options;const filename=context.filename;if(allowInTests&&(0,utils_1.isTestFile)(filename)){return{}}const isNoneAlgorithm=value=>{return value.toLowerCase()==="none"};const checkOptionsForNone=optionsNode=>{for(const prop of optionsNode.properties){if(prop.type!=="Property"||prop.key.type!=="Identifier"){continue}const keyName=prop.key.name;if(keyName!=="algorithms"&&keyName!=="algorithm"&&keyName!=="alg"){continue}if(prop.value.type==="Literal"&&typeof prop.value.value==="string"){if(isNoneAlgorithm(prop.value.value)){context.report({node:prop.value,messageId:"algorithmNone"})}}if(prop.value.type==="ArrayExpression"){if(prop.value.elements.length===0){context.report({node:prop.value,messageId:"emptyAlgorithms"});continue}for(const elem of prop.value.elements){if(elem&&elem.type==="Literal"&&typeof elem.value==="string"&&isNoneAlgorithm(elem.value)){context.report({node:elem,messageId:"algorithmNoneInArray"})}}}}};function isDecodeCall(node){const callee=node.callee;if(callee.type!=="MemberExpression")return false;if(callee.property.type!=="Identifier")return false;if(callee.property.name!=="decode")return false;if(callee.object.type==="Identifier"){const name=callee.object.name.toLowerCase();return name==="jwt"||name==="jsonwebtoken"}return false}return{CallExpression(node){if(isDecodeCall(node)){context.report({node,messageId:"decodeWithoutVerify"});return}if(!(0,utils_1.isVerifyOperation)(node)&&!(0,utils_1.isSignOperation)(node)){return}const optionsArg=(0,utils_1.getOptionsArgument)(node,2);if(optionsArg){checkOptionsForNone(optionsArg)}}}}});exports.default=exports.noAlgorithmNone;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDecodeWithoutVerify=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.noDecodeWithoutVerify=(0,eslint_devkit_1.createRule)({name:"no-decode-without-verify",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-decode-without-verify.md",description:"Disallow trusting decoded JWT payload without signature verification",cwe:"CWE-345",cvss:7.5},fixable:void 0,hasSuggestions:false,messages:{decodeWithoutVerify:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Decoded JWT Without Verification",cwe:"CWE-345",description:"jwt.decode() returns payload without verifying signature - data can be forged",severity:"HIGH",fix:"Use jwt.verify(token, secret) instead of jwt.decode(token)",documentationLink:"https://owasp.org/API-Security/0xa7-security-misconfiguration/"}),jwtDecodeLibrary:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"jwt-decode Library Usage",cwe:"CWE-345",description:"jwt-decode library only decodes tokens, never verifies signatures",severity:"HIGH",fix:"Use jsonwebtoken.verify() or jose.jwtVerify() for verification",documentationLink:"https://www.npmjs.com/package/jwt-decode"}),useVerifyInstead:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Verify Instead",description:"Replace decode with verify to ensure authenticity",severity:"LOW",fix:'jwt.verify(token, secret, { algorithms: ["RS256"] })',documentationLink:"https://www.npmjs.com/package/jsonwebtoken"})},schema:[{type:"object",properties:{allowHeaderInspection:{type:"boolean",default:false,description:"Allow decode() for reading header before verification"},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:["@decoded-header-only","@verified-separately"]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{allowHeaderInspection:false,trustedSanitizers:[],trustedAnnotations:["@decoded-header-only","@verified-separately"],strictMode:false}],create(context){const options=context.options[0]??{};const{trustedAnnotations=[]}=options;const isJwtDecodeLibrary=node=>{if(node.callee.type==="Identifier"&&(node.callee.name==="jwtDecode"||node.callee.name==="jwt_decode")){return true}return false};return{CallExpression(node){if((0,utils_1.isDecodeOperation)(node)){if((0,eslint_devkit_1.hasSafeAnnotation)(node,context,trustedAnnotations)){return}context.report({node,messageId:"decodeWithoutVerify"});return}if(isJwtDecodeLibrary(node)){if((0,eslint_devkit_1.hasSafeAnnotation)(node,context,trustedAnnotations)){return}context.report({node,messageId:"jwtDecodeLibrary"})}}}}});exports.default=exports.noDecodeWithoutVerify;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noHardcodedSecret=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.noHardcodedSecret=(0,eslint_devkit_1.createRule)({name:"no-hardcoded-secret",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-hardcoded-secret.md",description:"Disallow hardcoded secrets in JWT sign/verify operations",cwe:"CWE-798",cvss:9.8},fixable:void 0,hasSuggestions:false,messages:{hardcodedSecret:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Hardcoded JWT Secret",cwe:"CWE-798",description:"Secret key hardcoded in source code can be extracted from repositories",severity:"HIGH",fix:"Use process.env.JWT_SECRET or secure secret management",documentationLink:"https://cwe.mitre.org/data/definitions/798.html"}),useEnvVariable:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Environment Variable",description:"Replace hardcoded secret with environment variable",severity:"LOW",fix:"process.env.JWT_SECRET",documentationLink:"https://12factor.net/config"})},schema:[{type:"object",properties:{envPatterns:{type:"array",items:{type:"string"},default:[],description:"Patterns that indicate safe environment variable usage"},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{envPatterns:[],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const isHardcodedString=node=>{if(node.type==="Literal"&&typeof node.value==="string"){return true}if(node.type==="TemplateLiteral"&&node.expressions.length===0){return true}return false};const resolveConstLiteralValue=node=>{if(node.type!=="Identifier")return null;const scope=context.sourceCode.getScope?.(node)??null;if(!scope)return null;const variable=scope.references.find(r=>r.identifier===node)?.resolved;const def=variable?.defs[0];if(!def||def.type!=="Variable")return null;const decl=def.parent;if(decl?.type!=="VariableDeclaration"||decl.kind!=="const")return null;return def.node.init??null};const isSafeKeySource=node=>{if((0,utils_1.isEnvVariable)(node)){return true}if(node.type==="CallExpression"){return true}if(node.type==="AwaitExpression"){return true}if(node.type==="Identifier"){const init=resolveConstLiteralValue(node);if(init&&isHardcodedString(init)){return false}return true}if(node.type==="MemberExpression"&&!isHardcodedString(node)){return true}return false};const isHardcodedStringOrResolvedConst=node=>{if(isHardcodedString(node))return true;const init=resolveConstLiteralValue(node);return init?isHardcodedString(init):false};return{CallExpression(node){if(!(0,utils_1.isSignOperation)(node)&&!(0,utils_1.isVerifyOperation)(node)){return}if(node.arguments.length<2){return}const secretArg=node.arguments[1];if(isSafeKeySource(secretArg)){return}if(isHardcodedStringOrResolvedConst(secretArg)){context.report({node:secretArg,messageId:"hardcodedSecret"})}}}}});exports.default=exports.noHardcodedSecret;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSensitivePayload=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.noSensitivePayload=(0,eslint_devkit_1.createRule)({name:"no-sensitive-payload",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-sensitive-payload.md",description:"Prevent storing sensitive data in JWT payload which is only base64-encoded",cwe:"CWE-359",cvss:5},fixable:void 0,hasSuggestions:false,messages:{sensitivePayloadField:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Sensitive Data in JWT Payload",cwe:"CWE-359",description:'JWT payloads are not encrypted - sensitive data like "{{fieldName}}" can be read by anyone',severity:"MEDIUM",fix:"Store sensitive data server-side, reference by ID in token",documentationLink:"https://tools.ietf.org/html/rfc8725"})},schema:[{type:"object",properties:{additionalSensitiveFields:{type:"array",items:{type:"string"},default:[],description:"Additional field names to flag as sensitive"},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{additionalSensitiveFields:[],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const options=context.options[0]??{};const additionalFields=options.additionalSensitiveFields??[];const allSensitiveFields=new Set([...utils_1.SENSITIVE_PAYLOAD_FIELDS,...additionalFields]);return{CallExpression(node){if(!(0,utils_1.isSignOperation)(node)){return}if(node.arguments.length<1){return}const payloadArg=node.arguments[0];if(payloadArg.type==="ObjectExpression"){for(const prop of payloadArg.properties){if(prop.type==="Property"&&prop.key.type==="Identifier"){const fieldName=prop.key.name.toLowerCase();if(allSensitiveFields.has(fieldName)){context.report({node:prop,messageId:"sensitivePayloadField",data:{fieldName:prop.key.name}})}}}}}}}});exports.default=exports.noSensitivePayload;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTimestampManipulation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.noTimestampManipulation=(0,eslint_devkit_1.createRule)({name:"no-timestamp-manipulation",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-timestamp-manipulation.md",description:"Prevent disabling automatic timestamp generation which enables replay attacks",cwe:"CWE-294",cvss:7.5},fixable:void 0,hasSuggestions:false,messages:{timestampDisabled:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Timestamp Generation Disabled",cwe:"CWE-294",description:"noTimestamp:true disables iat claim, enabling replay attacks",severity:"HIGH",fix:"Remove noTimestamp option or set to false",documentationLink:"https://securitypattern.com/post/jwt-back-to-the-future"}),noTimestampTrue:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Back to the Future Attack Vector",cwe:"CWE-294",description:"Disabling timestamps allows attackers to forge tokens valid in the future",severity:"HIGH",fix:"Always include iat claim for freshness validation",documentationLink:"https://securitypattern.com/post/jwt-back-to-the-future"})},schema:[{type:"object",properties:{trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){return{CallExpression(node){if(!(0,utils_1.isSignOperation)(node)){return}const optionsArg=(0,utils_1.getOptionsArgument)(node,2);if(!optionsArg){return}if((0,utils_1.hasOption)(optionsArg,"noTimestamp")){const value=(0,utils_1.getOptionValue)(optionsArg,"noTimestamp");if(value?.type==="Literal"&&value.value===true){context.report({node:value,messageId:"noTimestampTrue"})}}}}}});exports.default=exports.noTimestampManipulation;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noWeakSecret=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");const WEAK_SECRET_PATTERNS=[/^secret$/i,/^password$/i,/^123456/,/^test/i,/^demo/i,/^example/i,/^changeme/i,/^default/i];exports.noWeakSecret=(0,eslint_devkit_1.createRule)({name:"no-weak-secret",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-weak-secret.md",description:"Require strong secrets (256+ bits) for HMAC-based JWT signing",cwe:"CWE-326",cvss:5.9},fixable:void 0,hasSuggestions:false,messages:{weakSecret:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Weak JWT Secret",cwe:"CWE-326",description:"JWT secret is too weak and can be brute-forced",severity:"HIGH",fix:'Use crypto.randomBytes(32).toString("hex") for secrets',documentationLink:"https://tools.ietf.org/html/rfc8725"}),shortSecret:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Short JWT Secret",cwe:"CWE-326",description:"JWT secret must be at least 32 characters (256 bits) for HS256",severity:"HIGH",fix:'Generate a secret with: crypto.randomBytes(32).toString("hex")',documentationLink:"https://tools.ietf.org/html/rfc8725"}),useStrongSecret:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Use Strong Secret",description:"Replace with cryptographically strong secret",severity:"LOW",fix:"Use process.env.JWT_SECRET with a 256-bit random value",documentationLink:"https://nodejs.org/api/crypto.html"})},schema:[{type:"object",properties:{minSecretLength:{type:"integer",default:32,minimum:16,description:"Minimum secret length in characters"},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{minSecretLength:32,trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const options=context.options[0]??{};const{minSecretLength=32}=options;const isKnownWeakPattern=value=>{return WEAK_SECRET_PATTERNS.some(pattern=>pattern.test(value))};const checkSecret=secretNode=>{if((0,utils_1.isEnvVariable)(secretNode)){return}if(secretNode.type==="Literal"&&typeof secretNode.value==="string"){const secretValue=secretNode.value;if(isKnownWeakPattern(secretValue)){context.report({node:secretNode,messageId:"weakSecret"});return}if(secretValue.length<minSecretLength){context.report({node:secretNode,messageId:"shortSecret",data:{length:String(secretValue.length),minLength:String(minSecretLength)}})}}if(secretNode.type==="TemplateLiteral"&&secretNode.quasis.length===1){const rawValue=secretNode.quasis[0].value.raw;if(isKnownWeakPattern(rawValue)||rawValue.length<minSecretLength){context.report({node:secretNode,messageId:"weakSecret"})}}};return{CallExpression(node){if(!(0,utils_1.isSignOperation)(node)&&!(0,utils_1.isVerifyOperation)(node)){return}if(node.arguments.length>=2){checkSecret(node.arguments[1])}}}}});exports.default=exports.noWeakSecret;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireAlgorithmWhitelist=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.requireAlgorithmWhitelist=(0,eslint_devkit_1.createRule)({name:"require-algorithm-whitelist",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-algorithm-whitelist.md",description:"Require explicit algorithm specification in JWT verify operations",cwe:"CWE-757",cvss:7.5},fixable:void 0,hasSuggestions:false,messages:{missingAlgorithmWhitelist:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Algorithm Whitelist",cwe:"CWE-757",description:"JWT verification without explicit algorithms trusts the token header",severity:"HIGH",fix:'Add algorithms: ["RS256"] or algorithms: ["ES256"] to options',documentationLink:"https://tools.ietf.org/html/rfc8725"}),addAlgorithmWhitelist:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Add Algorithm Whitelist",description:"Specify allowed algorithms explicitly",severity:"LOW",fix:'jwt.verify(token, key, { algorithms: ["RS256"] })',documentationLink:"https://tools.ietf.org/html/rfc8725"})},schema:[{type:"object",properties:{recommendedAlgorithms:{type:"array",items:{type:"string"},default:["RS256","ES256"],description:"Algorithms to suggest in auto-fix"},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{recommendedAlgorithms:["RS256","ES256"],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){return{CallExpression(node){if(!(0,utils_1.isVerifyOperation)(node)){return}if(node.arguments.length<2){return}const optionsArg=(0,utils_1.getOptionsArgument)(node,2);if(!optionsArg){context.report({node,messageId:"missingAlgorithmWhitelist"});return}const hasAlgorithms=(0,utils_1.hasOption)(optionsArg,"algorithms")||(0,utils_1.hasOption)(optionsArg,"algorithm")||(0,utils_1.hasOption)(optionsArg,"alg");if(!hasAlgorithms){context.report({node:optionsArg,messageId:"missingAlgorithmWhitelist"})}}}}});exports.default=exports.requireAlgorithmWhitelist;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireAudienceValidation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.requireAudienceValidation=(0,eslint_devkit_1.createRule)({name:"require-audience-validation",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-audience-validation.md",description:"Require audience (aud) claim validation in JWT verify operations",cwe:"CWE-287",cvss:9.8},fixable:void 0,hasSuggestions:false,messages:{missingAudienceValidation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Audience Validation",cwe:"CWE-287",description:"JWT verification without audience validation accepts tokens intended for other services",severity:"MEDIUM",fix:'Add audience option: { audience: "https://api.example.com" }',documentationLink:"https://tools.ietf.org/html/rfc8725"})},schema:[{type:"object",properties:{knownAudiences:{type:"array",items:{type:"string"},default:[],description:"Known valid audiences to suggest"},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{knownAudiences:[],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){return{CallExpression(node){if(!(0,utils_1.isVerifyOperation)(node)){return}if(node.arguments.length<2){return}const optionsArg=(0,utils_1.getOptionsArgument)(node,2);if(!optionsArg){context.report({node,messageId:"missingAudienceValidation"});return}if(!(0,utils_1.hasOption)(optionsArg,"audience")&&!(0,utils_1.hasOption)(optionsArg,"aud")){context.report({node:optionsArg,messageId:"missingAudienceValidation"})}}}}});exports.default=exports.requireAudienceValidation;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireExpiration=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.requireExpiration=(0,eslint_devkit_1.createRule)({name:"require-expiration",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-expiration.md",description:"Require expiration claim (exp) or expiresIn option in JWT signing",cwe:"CWE-613",cvss:5.4},hasSuggestions:true,messages:{missingExpiration:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing JWT Expiration",cwe:"CWE-613",description:"JWT without expiration is valid forever, increasing exposure window",severity:"MEDIUM",fix:'Add expiresIn: "1h" or exp claim to payload',documentationLink:"https://tools.ietf.org/html/rfc8725"}),addExpiration:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.INFO,issueName:"Add Expiration",description:"Add expiration to limit token lifetime",severity:"LOW",fix:'jwt.sign(payload, secret, { expiresIn: "1h" })',documentationLink:"https://www.npmjs.com/package/jsonwebtoken"})},schema:[{type:"object",properties:{maxExpirationSeconds:{type:"integer",default:86400,description:"Maximum allowed expiration time in seconds (24h default)"},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{maxExpirationSeconds:86400,trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const payloadHasExp=payloadNode=>{if(payloadNode.type!=="ObjectExpression"){return false}return payloadNode.properties.some(prop=>prop.type==="Property"&&prop.key.type==="Identifier"&&prop.key.name==="exp")};return{CallExpression(node){if(!(0,utils_1.isSignOperation)(node)){return}if(node.arguments.length<1){return}const payloadArg=node.arguments[0];const optionsArg=(0,utils_1.getOptionsArgument)(node,2);if(payloadHasExp(payloadArg)){return}if(optionsArg&&(0,utils_1.hasOption)(optionsArg,"expiresIn")){return}const optionsArg3=node.arguments[2];context.report({node,messageId:"missingExpiration",suggest:[{messageId:"addExpiration",fix(fixer){const sourceCode=context.sourceCode;if(optionsArg3&&optionsArg3.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){const openBrace=sourceCode.getFirstToken(optionsArg3);return fixer.insertTextAfter(openBrace," expiresIn: '1h',")}const lastArg=node.arguments[node.arguments.length-1];return fixer.insertTextAfter(lastArg,", { expiresIn: '1h' }")}}]})}}}});exports.default=exports.requireExpiration;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireIssuedAt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.requireIssuedAt=(0,eslint_devkit_1.createRule)({name:"require-issued-at",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-issued-at.md",description:"Require iat (issued at) claim for token freshness validation",cwe:"CWE-294",cvss:5},fixable:void 0,hasSuggestions:false,messages:{missingIssuedAt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Issued At Claim",cwe:"CWE-294",description:"JWT without iat claim cannot be validated for freshness, enabling replay attacks",severity:"MEDIUM",fix:"Add iat claim to payload or use library option that adds it automatically",documentationLink:"https://securitypattern.com/post/jwt-back-to-the-future"})},schema:[{type:"object",properties:{trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){const payloadHasIat=payloadNode=>{if(payloadNode.type!=="ObjectExpression"){return false}return payloadNode.properties.some(prop=>prop.type==="Property"&&prop.key.type==="Identifier"&&prop.key.name==="iat")};return{CallExpression(node){if(!(0,utils_1.isSignOperation)(node)){return}if(node.arguments.length<1){return}const payloadArg=node.arguments[0];const optionsArg=(0,utils_1.getOptionsArgument)(node,2);if(payloadHasIat(payloadArg)){return}if(optionsArg&&(0,utils_1.hasOption)(optionsArg,"noTimestamp")){context.report({node:optionsArg,messageId:"missingIssuedAt"});return}}}}});exports.default=exports.requireIssuedAt;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireIssuerValidation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.requireIssuerValidation=(0,eslint_devkit_1.createRule)({name:"require-issuer-validation",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-issuer-validation.md",description:"Require issuer (iss) claim validation in JWT verify operations",cwe:"CWE-287",cvss:9.8},fixable:void 0,hasSuggestions:false,messages:{missingIssuerValidation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Issuer Validation",cwe:"CWE-287",description:"JWT verification without issuer validation accepts tokens from any source",severity:"MEDIUM",fix:'Add issuer option: { issuer: "https://auth.example.com" }',documentationLink:"https://tools.ietf.org/html/rfc8725"})},schema:[{type:"object",properties:{knownIssuers:{type:"array",items:{type:"string"},default:[],description:"Known valid issuers to suggest"},trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{knownIssuers:[],trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){return{CallExpression(node){if(!(0,utils_1.isVerifyOperation)(node)){return}if(node.arguments.length<2){return}const optionsArg=(0,utils_1.getOptionsArgument)(node,2);if(!optionsArg){context.report({node,messageId:"missingIssuerValidation"});return}if(!(0,utils_1.hasOption)(optionsArg,"issuer")&&!(0,utils_1.hasOption)(optionsArg,"iss")){context.report({node:optionsArg,messageId:"missingIssuerValidation"})}}}}});exports.default=exports.requireIssuerValidation;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireMaxAge=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const utils_1=require("../../utils");exports.requireMaxAge=(0,eslint_devkit_1.createRule)({name:"require-max-age",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/require-max-age.md",description:"Require maxAge option in verify operations to enforce token freshness",cwe:"CWE-294",cvss:5},fixable:void 0,hasSuggestions:false,messages:{missingMaxAge:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Missing Token Age Validation",cwe:"CWE-294",description:"Without maxAge, tokens can be replayed long after issuance (Back to the Future attack)",severity:"MEDIUM",fix:'Add maxAge option: { maxAge: "1h" } to limit token lifetime',documentationLink:"https://securitypattern.com/post/jwt-back-to-the-future"})},schema:[{type:"object",properties:{trustedSanitizers:{type:"array",items:{type:"string"},default:[]},trustedAnnotations:{type:"array",items:{type:"string"},default:[]},strictMode:{type:"boolean",default:false}},additionalProperties:false}]},defaultOptions:[{trustedSanitizers:[],trustedAnnotations:[],strictMode:false}],create(context){return{CallExpression(node){if(!(0,utils_1.isVerifyOperation)(node)){return}if(node.arguments.length<2){return}const optionsArg=(0,utils_1.getOptionsArgument)(node,2);if(!optionsArg){context.report({node,messageId:"missingMaxAge"});return}if(!(0,utils_1.hasOption)(optionsArg,"maxAge")&&!(0,utils_1.hasOption)(optionsArg,"clockTolerance")){context.report({node:optionsArg,messageId:"missingMaxAge"})}}}}});exports.default=exports.requireMaxAge;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.JWT_METHODS=exports.SENSITIVE_PAYLOAD_FIELDS=exports.SECURE_ALGORITHMS=exports.SYMMETRIC_ALGORITHMS=exports.INSECURE_ALGORITHMS=exports.JWT_LIBRARIES=void 0;exports.isJwtLibraryCall=isJwtLibraryCall;exports.isStringLiteral=isStringLiteral;exports.extractAlgorithms=extractAlgorithms;exports.hasOption=hasOption;exports.getOptionValue=getOptionValue;exports.isWeakSecret=isWeakSecret;exports.isEnvVariable=isEnvVariable;exports.isSignOperation=isSignOperation;exports.isVerifyOperation=isVerifyOperation;exports.isDecodeOperation=isDecodeOperation;exports.getOptionsArgument=getOptionsArgument;exports.isTestFile=isTestFile;exports.JWT_LIBRARIES={JSONWEBTOKEN:"jsonwebtoken",JOSE:"jose",EXPRESS_JWT:"express-jwt",NESTJS_JWT:"@nestjs/jwt",JWKS_RSA:"jwks-rsa",JWT_DECODE:"jwt-decode"};exports.INSECURE_ALGORITHMS=new Set(["none","None","NONE","HS256","HS384","HS512"]);exports.SYMMETRIC_ALGORITHMS=new Set(["HS256","HS384","HS512"]);exports.SECURE_ALGORITHMS=new Set(["RS256","RS384","RS512","ES256","ES384","ES512","PS256","PS384","PS512","EdDSA"]);exports.SENSITIVE_PAYLOAD_FIELDS=new Set(["password","passwd","pwd","pass","secret","apikey","api_key","api-key","apisecret","api_secret","api-secret","accesstoken","access_token","access-token","refreshtoken","refresh_token","refresh-token","bearertoken","bearer_token","bearer-token","privatekey","private_key","private-key","secretkey","secret_key","secret-key","email","emailaddress","email_address","email-address","phone","phonenumber","phone_number","phone-number","ssn","socialsecuritynumber","social_security_number","dob","dateofbirth","date_of_birth","birthdate","address","streetaddress","street_address","creditcard","credit_card","credit-card","cardnumber","card_number","card-number","cvv","cvc","securitycode","security_code","pin","pincode","pin_code","bankaccount","bank_account","bank-account","accountnumber","account_number","account-number","routingnumber","routing_number","routing-number"]);exports.JWT_METHODS={SIGN:new Set(["sign","signJWT","SignJWT"]),VERIFY:new Set(["verify","verifyJWT","jwtVerify"]),DECODE:new Set(["decode","jwtDecode","decodeJWT"])};function isJwtLibraryCall(node,targetMethods){if(node.callee.type==="MemberExpression"){const property=node.callee.property;if(property.type==="Identifier"){return targetMethods.has(property.name)}}if(node.callee.type==="Identifier"){return targetMethods.has(node.callee.name)}return false}function isStringLiteral(node,value){if(node.type!=="Literal"||typeof node.value!=="string"){return false}return value===void 0||node.value===value}function extractAlgorithms(optionsNode){const algorithms=[];for(const prop of optionsNode.properties){if(prop.type!=="Property"||prop.key.type!=="Identifier"){continue}const keyName=prop.key.name;if(keyName!=="algorithms"&&keyName!=="algorithm"&&keyName!=="alg"){continue}if(prop.value.type==="Literal"&&typeof prop.value.value==="string"){algorithms.push(prop.value.value)}if(prop.value.type==="ArrayExpression"){for(const elem of prop.value.elements){if(elem&&elem.type==="Literal"&&typeof elem.value==="string"){algorithms.push(elem.value)}}}}return algorithms}function hasOption(optionsNode,optionName){return optionsNode.properties.some(prop=>prop.type==="Property"&&prop.key.type==="Identifier"&&prop.key.name===optionName)}function getOptionValue(optionsNode,optionName){for(const prop of optionsNode.properties){if(prop.type==="Property"&&prop.key.type==="Identifier"&&prop.key.name===optionName){return prop.value}}return void 0}function isWeakSecret(node,minLength=32){if(node.type==="Literal"&&typeof node.value==="string"){return node.value.length<minLength}return false}function isEnvVariable(node){if(node.type==="MemberExpression"&&node.object.type==="MemberExpression"&&node.object.object.type==="Identifier"&&node.object.object.name==="process"&&node.object.property.type==="Identifier"&&node.object.property.name==="env"){return true}return false}function isSignOperation(node){return isJwtLibraryCall(node,exports.JWT_METHODS.SIGN)}function isVerifyOperation(node){return isJwtLibraryCall(node,exports.JWT_METHODS.VERIFY)}function isDecodeOperation(node){return isJwtLibraryCall(node,exports.JWT_METHODS.DECODE)}function getOptionsArgument(node,optionsIndex=2){const arg=node.arguments[optionsIndex];if(arg&&arg.type==="ObjectExpression"){return arg}return void 0}function isTestFile(filename){return/\.(test|spec)\.[jt]sx?$/.test(filename)||/__(tests?|mocks?)__/.test(filename)||/\/(tests?|specs?|__tests__)\//.test(filename)}
|
package/AGENTS.md
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# AGENTS.md
|
|
2
|
-
|
|
3
|
-
> Context for AI coding agents working on eslint-plugin-jwt-security
|
|
4
|
-
|
|
5
|
-
## Setup Commands
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# Install dependencies (from monorepo root)
|
|
9
|
-
npm install
|
|
10
|
-
|
|
11
|
-
# Build this package
|
|
12
|
-
nx build eslint-plugin-jwt-security
|
|
13
|
-
|
|
14
|
-
# Run tests
|
|
15
|
-
nx test eslint-plugin-jwt-security
|
|
16
|
-
|
|
17
|
-
# Run tests with coverage
|
|
18
|
-
nx test eslint-plugin-jwt-security --coverage
|
|
19
|
-
|
|
20
|
-
# Lint this package
|
|
21
|
-
nx lint eslint-plugin-jwt-security
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Code Style
|
|
25
|
-
|
|
26
|
-
- TypeScript strict mode with `@interlace/eslint-devkit` types
|
|
27
|
-
- Use `AST_NODE_TYPES` constants, never string literals for node types
|
|
28
|
-
- Use `formatLLMMessage()` for all rule error messages
|
|
29
|
-
- Include CWE, CVSS, OWASP in every security message
|
|
30
|
-
- Use `c8 ignore` comments with documented reasons for untestable code
|
|
31
|
-
- Single-pass AST traversal patterns (O(n) complexity)
|
|
32
|
-
|
|
33
|
-
## Testing Instructions
|
|
34
|
-
|
|
35
|
-
- Tests use `@typescript-eslint/rule-tester` with Vitest
|
|
36
|
-
- Each rule has `index.ts` (implementation) and `*.test.ts` (tests) in same directory
|
|
37
|
-
- Run specific rule test: `nx test eslint-plugin-jwt-security --testPathPattern="no-algorithm-none"`
|
|
38
|
-
- Coverage target: ≥90% lines, ≥95% functions
|
|
39
|
-
- All tests must pass before committing
|
|
40
|
-
|
|
41
|
-
## Project Structure
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
src/
|
|
45
|
-
├── index.ts # Plugin entry, exports rules and configs
|
|
46
|
-
└── rules/ # 13 rule directories
|
|
47
|
-
└── [rule-name]/
|
|
48
|
-
├── index.ts # Rule implementation
|
|
49
|
-
└── *.test.ts # Rule tests
|
|
50
|
-
docs/rules/ # Markdown documentation per rule
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Plugin Purpose
|
|
54
|
-
|
|
55
|
-
Comprehensive JWT security ESLint plugin with **13 rules** covering algorithm attacks, replay prevention, and claim validation. Targets 6 major Node.js JWT libraries.
|
|
56
|
-
|
|
57
|
-
## Rule Categories
|
|
58
|
-
|
|
59
|
-
| Category | Rules | CWEs |
|
|
60
|
-
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ |
|
|
61
|
-
| Algorithm Attacks | `no-algorithm-none`, `no-algorithm-confusion` | 347 |
|
|
62
|
-
| Verification & Secrets | `require-algorithm-whitelist`, `no-decode-without-verify`, `no-weak-secret`, `no-hardcoded-secret`, `no-timestamp-manipulation` | 326, 345, 757, 798 |
|
|
63
|
-
| Claims | `require-expiration`, `require-issued-at`, `require-issuer-validation`, `require-audience-validation`, `require-max-age`, `no-sensitive-payload` | 287, 294, 359, 613 |
|
|
64
|
-
|
|
65
|
-
## Library Detection
|
|
66
|
-
|
|
67
|
-
The plugin detects these method calls:
|
|
68
|
-
|
|
69
|
-
- **Sign**: `jwt.sign()`, `signJWT()`, `SignJWT()`
|
|
70
|
-
- **Verify**: `jwt.verify()`, `jwtVerify()`, `verifyJWT()`
|
|
71
|
-
- **Decode**: `jwt.decode()`, `jwtDecode()`, `decodeJWT()`
|
|
72
|
-
|
|
73
|
-
## Common Fix Patterns
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// Algorithm None Attack (CVE-2022-23540)
|
|
77
|
-
// BAD: jwt.verify(token, secret, { algorithms: ['none'] })
|
|
78
|
-
// GOOD: jwt.verify(token, secret, { algorithms: ['RS256'] })
|
|
79
|
-
|
|
80
|
-
// Algorithm Confusion
|
|
81
|
-
// BAD: jwt.verify(token, publicKey, { algorithms: ['HS256'] })
|
|
82
|
-
// GOOD: jwt.verify(token, publicKey, { algorithms: ['RS256'] })
|
|
83
|
-
|
|
84
|
-
// Hardcoded Secret
|
|
85
|
-
// BAD: jwt.sign(payload, 'my-secret-key')
|
|
86
|
-
// GOOD: jwt.sign(payload, process.env.JWT_SECRET)
|
|
87
|
-
|
|
88
|
-
// Missing Expiration
|
|
89
|
-
// BAD: jwt.sign({ sub: 'user' }, secret)
|
|
90
|
-
// GOOD: jwt.sign({ sub: 'user' }, secret, { expiresIn: '1h' })
|
|
91
|
-
|
|
92
|
-
// Replay Attack Prevention
|
|
93
|
-
// BAD: jwt.sign(payload, secret, { noTimestamp: true })
|
|
94
|
-
// GOOD: jwt.sign(payload, secret)
|
|
95
|
-
// GOOD: jwt.verify(token, secret, { maxAge: '1h' })
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Security Considerations
|
|
99
|
-
|
|
100
|
-
- CWE coverage: 287, 294, 326, 345, 347, 359, 613, 757, 798
|
|
101
|
-
- Detects CVE-2022-23540 (Algorithm None attack)
|
|
102
|
-
- Covers 6 major JWT libraries: jsonwebtoken, jose, jwt-decode, node-jose, jwt-simple, jsonwebtoken-esm
|
package/CHANGELOG.md
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
## [2.2.3] - 2026-02-08
|
|
2
|
-
|
|
3
|
-
## 2.2.9
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- [#294](https://github.com/ofri-peretz/eslint/pull/294) [`659f6dc`](https://github.com/ofri-peretz/eslint/commit/659f6dc0181b03b675f72b5949fcf123dd066358) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Rewrite `description` and `keywords` on every published package for npm search discovery. npm ranks on name, description, and keywords, and the registry only picks up these fields at publish — so this is metadata-only and takes effect for each package on its next release.
|
|
8
|
-
|
|
9
|
-
**Descriptions now lead with the search phrase.** Every one starts `ESLint plugin for <the thing you'd search>` instead of a brand-first or category-first framing, and names the concrete vulnerabilities the plugin actually detects. Three were corrected while doing so:
|
|
10
|
-
|
|
11
|
-
- `eslint-plugin-import-next` claimed "100x faster no-cycle detection". No 100x measurement exists: `CLAIMS.md` records **3.1x end-to-end** (8x in pure rule execution) on a 5,483-file React codebase, and the highest number in any benchmark result is 54.9x on the synthetic corpus. The description now states the real-codebase figure.
|
|
12
|
-
- `eslint-plugin-secure-coding` claimed SQL injection, XSS and CSRF coverage — none of which are its rules. It now names what it does detect: LDAP, XPath, XXE, GraphQL and template injection, unsafe deserialization, ReDoS, missing authentication, and PII in logs.
|
|
13
|
-
- `eslint-plugin-secure-coding` ("89 rules") and `eslint-plugin-react-a11y` ("37 rules") hard-coded rule counts that had drifted from reality. Counts are generated into `interlace-numbers.json`; hand-typed copies are removed rather than corrected.
|
|
14
|
-
|
|
15
|
-
**Keywords now match the vocabulary of the plugins that rank.** `eslint-plugin-security`, `eslint-plugin-jsx-a11y`, `eslint-plugin-n` and `eslint-plugin-import` all carry the `eslint` / `eslintplugin` / `eslint-plugin` trio — six of our packages were missing `eslintplugin`, and every one now carries all three plus `static-analysis`, `linting` and `code-quality`. Security plugins add `sast`, `appsec` and `vulnerability`; `node-security` and `secure-coding` also carry `nodesecurity`, the exact keyword `eslint-plugin-security` ranks on. Each plugin gained the CWE identifiers and attack names for what it detects (`cwe-78` command injection, `cwe-22` path traversal, `cwe-89` SQL injection, `cwe-79` XSS, `cwe-347` JWT algorithm confusion, `cwe-352` CSRF, `cwe-943` NoSQL injection), and `node-security` gained the crypto vocabulary it had been missing entirely despite absorbing the crypto rule set (`crypto`, `cryptography`, `weak-hash`, `md5`, `sha1`, `timing-attack`).
|
|
16
|
-
|
|
17
|
-
No rule behavior, exports, or configuration changes.
|
|
18
|
-
|
|
19
|
-
- Updated dependencies [[`e1cdf83`](https://github.com/ofri-peretz/eslint/commit/e1cdf83e3db761907f0ab06f7fc6c1f1da7513a5), [`659f6dc`](https://github.com/ofri-peretz/eslint/commit/659f6dc0181b03b675f72b5949fcf123dd066358)]:
|
|
20
|
-
- @interlace/eslint-devkit@1.4.3
|
|
21
|
-
|
|
22
|
-
## 2.2.8
|
|
23
|
-
|
|
24
|
-
### Patch Changes
|
|
25
|
-
|
|
26
|
-
- [#269](https://github.com/ofri-peretz/eslint/pull/269) [`7028fe2`](https://github.com/ofri-peretz/eslint/commit/7028fe2668a42266d831014184dcef70e73101ad) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - docs: dual-logo README header (Interlace mark + ESLint mark side by side) and closing Interlace footer — refreshes the README rendered on npmjs.com. No runtime changes.
|
|
27
|
-
|
|
28
|
-
- Updated dependencies [[`7028fe2`](https://github.com/ofri-peretz/eslint/commit/7028fe2668a42266d831014184dcef70e73101ad)]:
|
|
29
|
-
- @interlace/eslint-devkit@1.4.2
|
|
30
|
-
|
|
31
|
-
## 2.2.7
|
|
32
|
-
|
|
33
|
-
### Patch Changes
|
|
34
|
-
|
|
35
|
-
- [#252](https://github.com/ofri-peretz/eslint/pull/252) [`d67e395`](https://github.com/ofri-peretz/eslint/commit/d67e3953c2748ad36e6aebe0f24b1d04e518b4d0) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Fix Codecov badge showing "unknown" — switch from flag to component URL format
|
|
36
|
-
|
|
37
|
-
## 2.2.6
|
|
38
|
-
|
|
39
|
-
### Patch Changes
|
|
40
|
-
|
|
41
|
-
- [#225](https://github.com/ofri-peretz/eslint/pull/225) [`34ff5a8`](https://github.com/ofri-peretz/eslint/commit/34ff5a8e6f5126c5d1c0a524759e0af2b5476b46) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - CI-only: pin all coverage thresholds at 100% (integration target, merges last).
|
|
42
|
-
|
|
43
|
-
## 2.2.5
|
|
44
|
-
|
|
45
|
-
### Patch Changes
|
|
46
|
-
|
|
47
|
-
- [#213](https://github.com/ofri-peretz/eslint/pull/213) [`391dbe6`](https://github.com/ofri-peretz/eslint/commit/391dbe6b39f78d549379218567cb959649f8c614) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Align every security rule's `meta.docs.cvss` to the CVSS its finding actually
|
|
48
|
-
emits. The emitted machine-readable message sources its `CVSS:x` from
|
|
49
|
-
`CWE_MAPPING` via `formatLLMMessage` → `enrichFromCWE`, but the static
|
|
50
|
-
`meta.docs.cvss` documentation field had drifted on 45 rules across these 7
|
|
51
|
-
plugins — e.g. `no-hardcoded-credentials` documented `9.5` while emitting
|
|
52
|
-
`CVSS:9.8` (the value the published article and SARIF/LLM consumers already
|
|
53
|
-
read).
|
|
54
|
-
|
|
55
|
-
This corrects the **documentation metadata only** — no emitted finding changes.
|
|
56
|
-
Locked by `security-cvss-docs-consistency.lock.test.ts` (cross-plugin: every
|
|
57
|
-
security rule's `meta.docs.cvss` must equal the CVSS it emits), the
|
|
58
|
-
`no-hardcoded-credentials` rule lock (real ESLint `Linter` emission), and a
|
|
59
|
-
devkit `enrichFromCWE` contract test pinning `CWE-798 → 9.8`.
|
|
60
|
-
|
|
61
|
-
Follow-up (not in scope): 50 security rules document a CVSS that never appears
|
|
62
|
-
in any emitted message (their messages carry no CWE), and several rules emit the
|
|
63
|
-
generic CWE score where a rule-specific score may be warranted — both change
|
|
64
|
-
emitted output and are separate decisions.
|
|
65
|
-
|
|
66
|
-
## 2.2.4
|
|
67
|
-
|
|
68
|
-
### Patch Changes
|
|
69
|
-
|
|
70
|
-
- [#143](https://github.com/ofri-peretz/eslint/pull/143) [`213cde1`](https://github.com/ofri-peretz/eslint/commit/213cde190ff2aea49ca7c1b533170940f879d9b4) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - fix(no-missing-null-checks): eliminate 53 false positives via three new narrowing patterns
|
|
71
|
-
|
|
72
|
-
Rules that were recognized as null guards are now correctly identified as safe:
|
|
73
|
-
1. **Truthy if guard** — `if (obj) { obj.prop }` — direct truthy check proves non-null. Also covers chains: `if (response)` protects `response.data.items`.
|
|
74
|
-
2. **Short-circuit AND** — `obj && obj.prop` — right side of `&&` only runs when left is truthy.
|
|
75
|
-
3. **Ternary consequent** — `obj ? obj.prop : fallback` — truthy test guards the consequent.
|
|
76
|
-
|
|
77
|
-
Also: bumped `beforeAll` timeout to 30 seconds in 7 compatibility test files (`__compatibility__/*.spec.ts`). Native-addon packages routinely exceed the previous 10-second default on a cold ESM load.
|
|
78
|
-
|
|
79
|
-
- Updated dependencies [[`736a5fe`](https://github.com/ofri-peretz/eslint/commit/736a5fed47e673f6157ea900b29fe2a54e4bc7df)]:
|
|
80
|
-
- @interlace/eslint-devkit@1.4.1
|
|
81
|
-
|
|
82
|
-
### Bug Fixes
|
|
83
|
-
|
|
84
|
-
- align codecov component IDs with full package names ([2831b968](https://github.com/ofri-peretz/eslint/commit/2831b968))
|
|
85
|
-
|
|
86
|
-
### Documentation
|
|
87
|
-
|
|
88
|
-
- fix changelog header format across all packages ([c3a15082](https://github.com/ofri-peretz/eslint/commit/c3a15082))
|
|
89
|
-
|
|
90
|
-
### ❤️ Thank You
|
|
91
|
-
|
|
92
|
-
- Ofri Peretz
|
|
93
|
-
|
|
94
|
-
## [2.2.2] - 2026-02-06
|
|
95
|
-
|
|
96
|
-
### Bug Fixes
|
|
97
|
-
|
|
98
|
-
- align codecov component names and update docs components ([0a59a86c](https://github.com/ofri-peretz/eslint/commit/0a59a86c))
|
|
99
|
-
|
|
100
|
-
### ❤️ Thank You
|
|
101
|
-
|
|
102
|
-
- Ofri Peretz
|
|
103
|
-
|
|
104
|
-
## [2.2.1] - 2026-02-02
|
|
105
|
-
|
|
106
|
-
This was a version bump only for eslint-plugin-jwt-security to align it with other projects, there were no code changes.
|
|
107
|
-
|
|
108
|
-
# Changelog
|
|
109
|
-
|
|
110
|
-
All notable changes to eslint-plugin-jwt-security will be documented in this file.
|
|
111
|
-
|
|
112
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
113
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
114
|
-
|
|
115
|
-
## [1.0.0] - 2025-12-29
|
|
116
|
-
|
|
117
|
-
### Added
|
|
118
|
-
|
|
119
|
-
#### Core Security Rules (7)
|
|
120
|
-
|
|
121
|
-
- `no-algorithm-none` - Prevent `alg:"none"` attack (CVE-2022-23540, CWE-347)
|
|
122
|
-
- `no-algorithm-confusion` - Prevent RS256→HS256 key confusion (CWE-347)
|
|
123
|
-
- `require-algorithm-whitelist` - Require explicit algorithm specification (CWE-757)
|
|
124
|
-
- `no-decode-without-verify` - Prevent trusting decoded payloads (CWE-345)
|
|
125
|
-
- `no-weak-secret` - Require 256-bit minimum secrets (CWE-326)
|
|
126
|
-
- `no-hardcoded-secret` - Prevent secrets in source code (CWE-798)
|
|
127
|
-
- `require-expiration` - Require `exp` claim or `expiresIn` (CWE-613)
|
|
128
|
-
|
|
129
|
-
#### 2025 Research Rules (6)
|
|
130
|
-
|
|
131
|
-
Based on LightSEC 2025 "Back to the Future" attack research:
|
|
132
|
-
|
|
133
|
-
- `require-issued-at` - Require `iat` claim for freshness (CWE-294)
|
|
134
|
-
- `require-issuer-validation` - Require issuer validation (CWE-287)
|
|
135
|
-
- `require-audience-validation` - Require audience validation (CWE-287)
|
|
136
|
-
- `no-timestamp-manipulation` - Prevent `noTimestamp: true` (CWE-294)
|
|
137
|
-
- `require-max-age` - Require maxAge for replay prevention (CWE-294)
|
|
138
|
-
- `no-sensitive-payload` - Prevent PII in token payload (CWE-359)
|
|
139
|
-
|
|
140
|
-
#### Configuration Presets
|
|
141
|
-
|
|
142
|
-
- `recommended` - Balanced security (critical=error, high=warn)
|
|
143
|
-
- `strict` - Maximum security with all 13 rules enabled
|
|
144
|
-
- `legacy` - Migration mode with only critical rules
|
|
145
|
-
|
|
146
|
-
#### Library Support
|
|
147
|
-
|
|
148
|
-
- jsonwebtoken
|
|
149
|
-
- jose
|
|
150
|
-
- express-jwt
|
|
151
|
-
- @nestjs/jwt
|
|
152
|
-
- jwks-rsa
|
|
153
|
-
- jwt-decode
|
|
154
|
-
|
|
155
|
-
### Features
|
|
156
|
-
|
|
157
|
-
- AI-optimized error messages using `formatLLMMessage`
|
|
158
|
-
- CWE references for all rules (9 CWEs covered)
|
|
159
|
-
- OWASP Top 10 2021 coverage matrix (6 categories)
|
|
160
|
-
- Full TypeScript support
|
|
161
|
-
- Comprehensive test coverage (248 edge case tests)
|