Skip to content

Commit 267687d

Browse files
feat: add a new rule should-specify-forbid-unknown-values
1 parent 24b17b0 commit 267687d

File tree

7 files changed

+355
-2
lines changed

7 files changed

+355
-2
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ I have a bunch of rules here that are mostly for strict typing with decorators f
3030

3131
These rules are somewhat opinionated, but necessary for clean model generation if using an Open Api model generator.
3232

33+
### 2. Some security and best practices
34+
35+
There is a CVE for class-transformer when using random javascript objects. You need to be careful about configuring the ValidationPipe in NestJs. See
36+
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18413
37+
https://github.com/typestack/class-validator/issues/438
38+
3339
## Available rule index (more details are available for each rule below)
3440

3541
Nest Modules
@@ -83,6 +89,41 @@ Note: You can easily turn off all the swagger rules if you don't use swagger by
8389

8490
## Rule Details
8591

92+
### Rule: should-specify-forbid-unknown-values
93+
94+
This checks when if you are setting ValidationPipe parameters you set forbidUnknownValues to true.
95+
96+
e.g. this passes
97+
98+
```ts
99+
const validationPipeB = new ValidationPipe({
100+
forbidNonWhitelisted: true,
101+
forbidUnknownValues: true,
102+
});
103+
```
104+
105+
this fails because property is not set
106+
107+
```ts
108+
const validationPipeB = new ValidationPipe({
109+
forbidNonWhitelisted: true,
110+
});
111+
```
112+
113+
this fails because property is set to false
114+
115+
```ts
116+
const validationPipeB = new ValidationPipe({
117+
forbidNonWhitelisted: false,
118+
});
119+
```
120+
121+
this passes because the default values seem to work ok
122+
123+
```ts
124+
const validationPipeB = new ValidationPipe();
125+
```
126+
86127
### Rule: provided-injected-should-match-factory-parameters
87128

88129
Checks that there are the same number of injected items in a Provider that are passed to the factory method

src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ export = {
2020
"@darraghor/nestjs-typed/api-enum-property-best-practices": "error",
2121
"@darraghor/nestjs-typed/api-property-returning-array-should-set-array":
2222
"error",
23+
"@darraghor/nestjs-typed/should-specify-forbid-unknown-values": "error",
2324
},
2425
};

src/rules/apiEnumPropertyBestPractices/apiEnumPropertyBestPractices.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ const rule = createRule({
106106
needsEnumNameAdded: `Properties with enum should also specify an enumName property to keep generated models clean`,
107107
needsTypeRemoved: `Properties with enum should not specify a type property`,
108108
enumNameShouldMatchType: `The enumName should match the enum type provided`,
109-
randomTest: `FAIL: {{msgText}}`,
110109
},
111110
schema: [],
112111
hasSuggestions: false,

src/rules/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import controllerDecoratedHasApiTags from "./controllerDecoratedHasApiTags/contr
55
import apiMethodsShouldSpecifyApiResponse from "./apiMethodsShouldSpecifyApiResponse/apiMethodsShouldSpecifyApiResponse";
66
import apiEnumPropertyBestPractices from "./apiEnumPropertyBestPractices/apiEnumPropertyBestPractices";
77
import apiPropertyReturningArrayShouldSetArray from "./apiPropertyReturningArrayShouldSetArray/apiPropertyReturningArrayShouldSetArray";
8+
import shouldSpecifyForbidUnknownValues from "./shouldSpecifyForbidUnknownValues/shouldSpecifyForbidUnknownValuesRule";
89

910
const allRules = {
1011
"api-property-matches-property-optionality":
@@ -14,10 +15,11 @@ const allRules = {
1415
providedInjectedShouldMatchFactoryParameters,
1516
"controllers-should-supply-api-tags": controllerDecoratedHasApiTags,
1617
"api-method-should-specify-api-response":
17-
apiMethodsShouldSpecifyApiResponse,
18+
apiMethodsShouldSpecifyApiResponse,
1819
"api-enum-property-best-practices": apiEnumPropertyBestPractices,
1920
"api-property-returning-array-should-set-array":
2021
apiPropertyReturningArrayShouldSetArray,
22+
"should-specify-forbid-unknown-values": shouldSpecifyForbidUnknownValues,
2123
};
2224

2325
export default allRules;
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/* eslint-disable unicorn/filename-case */
2+
export const testCases = [
3+
{
4+
moduleCode: `
5+
const options = {
6+
forbidNonWhitelisted: true,
7+
forbidUnknownValues: true,
8+
} as ValidationPipeOptions;
9+
10+
const validationPipeA = new ValidationPipe(options);
11+
12+
const validationPipeB = new ValidationPipe({
13+
transform: true,
14+
skipMissingProperties: false,
15+
whitelist: true,
16+
forbidNonWhitelisted: true,
17+
forbidUnknownValues: true,
18+
});
19+
`,
20+
isNewExpressionTriggered: false,
21+
isVariableExpressionTriggered: false,
22+
message: "property is present in both scenarios",
23+
},
24+
{
25+
moduleCode: `
26+
const options = {
27+
forbidNonWhitelisted: true,
28+
} as ValidationPipeOptions;
29+
30+
const validationPipeA = new ValidationPipe(options);
31+
32+
const validationPipeB = new ValidationPipe({
33+
transform: true,
34+
skipMissingProperties: false,
35+
whitelist: true,
36+
forbidNonWhitelisted: true,
37+
forbidUnknownValues: true,
38+
});
39+
`,
40+
isNewExpressionTriggered: false,
41+
isVariableExpressionTriggered: true,
42+
message: "property is missing in options object variable",
43+
},
44+
{
45+
moduleCode: `
46+
const options = {
47+
forbidNonWhitelisted: true,
48+
} as ThisIsNotAValidationPipeOptionsClass;
49+
50+
const validationPipeA = new ValidationPipe(options);
51+
52+
const validationPipeB = new ValidationPipe({
53+
transform: true,
54+
skipMissingProperties: false,
55+
whitelist: true,
56+
forbidNonWhitelisted: true,
57+
forbidUnknownValues: true,
58+
});
59+
`,
60+
isNewExpressionTriggered: false,
61+
isVariableExpressionTriggered: false,
62+
message: "not a validation options class",
63+
},
64+
{
65+
moduleCode: `
66+
const options = {
67+
forbidNonWhitelisted: true,
68+
forbidUnknownValues: true,
69+
} as ValidationPipeOptions;
70+
71+
const validationPipeA = new ValidationPipe(options);
72+
73+
const validationPipeB = new ValidationPipe({
74+
transform: true,
75+
skipMissingProperties: false,
76+
whitelist: true,
77+
forbidNonWhitelisted: true
78+
});
79+
`,
80+
isNewExpressionTriggered: true,
81+
isVariableExpressionTriggered: false,
82+
message: "property is missing in parameter declaration",
83+
},
84+
{
85+
moduleCode: `
86+
const options = {
87+
forbidNonWhitelisted: true,
88+
forbidUnknownValues: true,
89+
} as ValidationPipeOptions;
90+
91+
const validationPipeA = new ValidationPipe(options);
92+
93+
const validationPipeB = new ThisIsNotAValidationPipeClass({
94+
transform: true,
95+
skipMissingProperties: false,
96+
whitelist: true,
97+
forbidNonWhitelisted: true
98+
});
99+
`,
100+
isNewExpressionTriggered: false,
101+
isVariableExpressionTriggered: false,
102+
message: "not the right class to trigger the rule",
103+
},
104+
{
105+
moduleCode: `
106+
const options = {
107+
forbidNonWhitelisted: true,
108+
forbidUnknownValues: true,
109+
} as ValidationPipeOptions;
110+
111+
const validationPipeA = new ValidationPipe(options);
112+
113+
const validationPipeB = new ValidationPipe({});
114+
`,
115+
isNewExpressionTriggered: true,
116+
isVariableExpressionTriggered: false,
117+
message: "empty options object should trigger",
118+
},
119+
{
120+
moduleCode: `
121+
const options = {
122+
forbidNonWhitelisted: true,
123+
forbidUnknownValues: true,
124+
} as ValidationPipeOptions;
125+
126+
const validationPipeA = new ValidationPipe(options);
127+
128+
const validationPipeB = new ValidationPipe();
129+
`,
130+
isNewExpressionTriggered: false,
131+
isVariableExpressionTriggered: false,
132+
message: "empty constructor should not trigger the rule",
133+
},
134+
];
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {testCases} from "./rule.testData";
2+
import {typedTokenHelpers} from "../../utils/typedTokenHelpers";
3+
import {
4+
fakeContext,
5+
fakeFilePath,
6+
} from "../../utils/nestModules/nestProvidedInjectableMapper.testData";
7+
8+
import {
9+
shouldTriggerForVariableDecleratorExpression,
10+
shouldTriggerNewExpressionHasProperty,
11+
} from "./shouldSpecifyForbidUnknownValuesRule";
12+
13+
// should probably be split up into multiple tests
14+
describe("shouldUseForbidUnknownRule", () => {
15+
test.each(testCases)(
16+
"is an expected response for %#",
17+
(testCase: {
18+
moduleCode: string;
19+
isNewExpressionTriggered: boolean;
20+
isVariableExpressionTriggered: boolean;
21+
message: string;
22+
}) => {
23+
const ast = typedTokenHelpers.parseStringToAst(
24+
testCase.moduleCode,
25+
fakeFilePath,
26+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
27+
fakeContext
28+
);
29+
30+
const isNewExpressionTriggered =
31+
shouldTriggerNewExpressionHasProperty(
32+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-explicit-any
33+
(ast as any).body[2].declarations[0].init
34+
);
35+
const isVariableExpressionTriggered =
36+
shouldTriggerForVariableDecleratorExpression(
37+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-explicit-any
38+
(ast as any).body[0].declarations[0]
39+
);
40+
41+
expect(isNewExpressionTriggered).toEqual(
42+
testCase.isNewExpressionTriggered
43+
);
44+
expect(isVariableExpressionTriggered).toEqual(
45+
testCase.isVariableExpressionTriggered
46+
);
47+
}
48+
);
49+
});

0 commit comments

Comments
 (0)