@appland/scanner 1.68.0 → 1.70.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/CHANGELOG.md +28 -0
- package/built/cli/scan/command.js +1 -1
- package/built/rules/deserializationOfUntrustedData.js +52 -35
- package/built/rules/jwtAlgorithmNone.js +47 -0
- package/built/rules/jwtUnverifiedSignature.js +84 -0
- package/built/rules/lib/analyzeDataFlow.js +80 -0
- package/built/rules/lib/util.js +1 -1
- package/doc/rules/deserialization-of-untrusted-data.md +28 -8
- package/doc/rules/jwt-algorithm-none.md +25 -0
- package/doc/rules/jwt-unverified-signature.md +24 -0
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
# [@appland/scanner-v1.70.0](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.69.1...@appland/scanner-v1.70.0) (2022-08-31)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Don't attempt to destructure an undefined value ([b46e358](https://github.com/applandinc/appmap-js/commit/b46e358cf0fd0cc56a7f465268f87f219ab13c55))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Add scan for presence of JWT signature verification ([a2b382b](https://github.com/applandinc/appmap-js/commit/a2b382bd571cfbc0fcdfa389ad382536f85eb671))
|
|
12
|
+
* Add scanner for usage of JWT with the `none` algorithm ([025ac89](https://github.com/applandinc/appmap-js/commit/025ac89f0538d5b4bfed7f36e3d09788f2a38076))
|
|
13
|
+
|
|
14
|
+
# [@appland/scanner-v1.69.1](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.69.0...@appland/scanner-v1.69.1) (2022-08-29)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* Add missing dependencies ([97a5d02](https://github.com/applandinc/appmap-js/commit/97a5d02ff161b52200430d2123d8d9ab62037220))
|
|
20
|
+
* Don't attempt to resolve a remote app ID if running in watch mode ([8f21ff1](https://github.com/applandinc/appmap-js/commit/8f21ff1a3bc86292f70a2cd1446f682e525869aa))
|
|
21
|
+
|
|
22
|
+
# [@appland/scanner-v1.69.0](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.68.0...@appland/scanner-v1.69.0) (2022-08-23)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* Track specific untrusted data in unsafe deserialization rule ([d14fd4f](https://github.com/applandinc/appmap-js/commit/d14fd4f65fcbabfebdaf0d10dcae71dc563bc1fa))
|
|
28
|
+
|
|
1
29
|
# [@appland/scanner-v1.68.0](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.67.0...@appland/scanner-v1.68.0) (2022-08-19)
|
|
2
30
|
|
|
3
31
|
|
|
@@ -75,7 +75,7 @@ exports.default = {
|
|
|
75
75
|
if (appmapDir)
|
|
76
76
|
yield (0, validateFile_1.default)('directory', appmapDir);
|
|
77
77
|
let appId = appIdArg;
|
|
78
|
-
if (!reportAllFindings)
|
|
78
|
+
if (!watch && !reportAllFindings)
|
|
79
79
|
appId = yield (0, resolveAppId_1.default)(appIdArg, appmapDir);
|
|
80
80
|
if (watch) {
|
|
81
81
|
const watchAppMapDir = appmapDir;
|
|
@@ -3,46 +3,62 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const models_1 = require("@appland/models");
|
|
7
6
|
const url_1 = require("url");
|
|
8
7
|
const parseRuleDescription_1 = __importDefault(require("./lib/parseRuleDescription"));
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
8
|
+
const analyzeDataFlow_1 = __importDefault(require("./lib/analyzeDataFlow"));
|
|
9
|
+
function valueHistory(value) {
|
|
10
|
+
const events = [];
|
|
11
|
+
const queue = [value];
|
|
12
|
+
for (;;) {
|
|
13
|
+
const current = queue.shift();
|
|
14
|
+
if (!current)
|
|
15
|
+
break;
|
|
16
|
+
const { origin, parents } = current;
|
|
17
|
+
if (!events.includes(origin))
|
|
18
|
+
events.push(origin);
|
|
19
|
+
queue.push(...parents);
|
|
20
|
+
}
|
|
21
|
+
return events;
|
|
22
|
+
}
|
|
23
|
+
function wasSanitized(value) {
|
|
24
|
+
return valueHistory(value).some(({ labels }) => labels.has(DeserializeSanitize));
|
|
22
25
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
function formatHistories(values) {
|
|
27
|
+
const histories = values.map(valueHistory);
|
|
28
|
+
return Object.fromEntries(histories.flatMap((history, input) => history.map((event, idx) => [`origin[${input}][${idx}]`, event])));
|
|
29
|
+
}
|
|
30
|
+
function label(name) {
|
|
31
|
+
return ({ labels }) => labels.has(name);
|
|
32
|
+
}
|
|
33
|
+
function matcher(startEvent) {
|
|
34
|
+
const flow = (0, analyzeDataFlow_1.default)([...(startEvent.message || [])], startEvent);
|
|
35
|
+
const results = [];
|
|
36
|
+
const sanitizedValues = new Set();
|
|
37
|
+
for (const [event, values] of flow) {
|
|
38
|
+
if (event.labels.has(DeserializeSanitize)) {
|
|
39
|
+
for (const v of values)
|
|
40
|
+
sanitizedValues.add(v);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!event.labels.has(DeserializeUnsafe))
|
|
44
|
+
continue;
|
|
45
|
+
const unsanitized = new Set(values.filter((v) => !(wasSanitized(v) || sanitizedValues.has(v))));
|
|
46
|
+
// remove any that have been passed into a safe deserialization function
|
|
47
|
+
for (const ancestor of event.ancestors().filter(label(DeserializeSafe))) {
|
|
48
|
+
for (const v of flow.get(ancestor) || []) {
|
|
49
|
+
unsanitized.delete(v);
|
|
40
50
|
}
|
|
41
51
|
}
|
|
52
|
+
const remaining = [...unsanitized];
|
|
53
|
+
if (remaining.length === 0)
|
|
54
|
+
continue;
|
|
55
|
+
results.push({
|
|
56
|
+
event: event,
|
|
57
|
+
message: `deserializes untrusted data: ${remaining.map(({ value: { value } }) => value)}`,
|
|
58
|
+
participatingEvents: formatHistories(remaining),
|
|
59
|
+
});
|
|
42
60
|
}
|
|
43
|
-
return
|
|
44
|
-
matcher,
|
|
45
|
-
};
|
|
61
|
+
return results;
|
|
46
62
|
}
|
|
47
63
|
const DeserializeUnsafe = 'deserialize.unsafe';
|
|
48
64
|
const DeserializeSafe = 'deserialize.safe';
|
|
@@ -53,11 +69,12 @@ exports.default = {
|
|
|
53
69
|
labels: [DeserializeUnsafe, DeserializeSafe, DeserializeSanitize],
|
|
54
70
|
impactDomain: 'Security',
|
|
55
71
|
enumerateScope: false,
|
|
72
|
+
scope: 'http_server_request',
|
|
56
73
|
references: {
|
|
57
74
|
'CWE-502': new url_1.URL('https://cwe.mitre.org/data/definitions/502.html'),
|
|
58
75
|
'Ruby Security': new url_1.URL('https://docs.ruby-lang.org/en/3.0/doc/security_rdoc.html'),
|
|
59
76
|
},
|
|
60
77
|
description: (0, parseRuleDescription_1.default)('deserializationOfUntrustedData'),
|
|
61
78
|
url: 'https://appland.com/docs/analysis/rules-reference.html#deserialization-of-untrusted-data',
|
|
62
|
-
build,
|
|
79
|
+
build: () => ({ matcher }),
|
|
63
80
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const parseRuleDescription_1 = __importDefault(require("./lib/parseRuleDescription"));
|
|
7
|
+
function getHeader(jwt) {
|
|
8
|
+
try {
|
|
9
|
+
const [header] = jwt.split('.');
|
|
10
|
+
const decodedHeader = Buffer.from(header, 'base64').toString('utf-8');
|
|
11
|
+
return JSON.parse(decodedHeader);
|
|
12
|
+
}
|
|
13
|
+
catch (_a) {
|
|
14
|
+
// the JWT is malformed
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
class JwtAlgoritmNoneLogic {
|
|
19
|
+
matcher(event) {
|
|
20
|
+
if (!event.returnValue)
|
|
21
|
+
return;
|
|
22
|
+
const matches = new Array();
|
|
23
|
+
const { value: jwt } = event.returnValue;
|
|
24
|
+
const header = getHeader(jwt);
|
|
25
|
+
if ((header === null || header === void 0 ? void 0 : header.alg) === 'none') {
|
|
26
|
+
matches.push({ event, message: 'Encoded JWT using the `none` algorithm' });
|
|
27
|
+
}
|
|
28
|
+
return matches;
|
|
29
|
+
}
|
|
30
|
+
where(event) {
|
|
31
|
+
return event.labels.has('jwt.encode');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
class JwtAlgoritmNone {
|
|
35
|
+
constructor() {
|
|
36
|
+
this.id = 'jwt-algorithm-none';
|
|
37
|
+
this.title = "JWT 'none' algorithm";
|
|
38
|
+
this.impactDomain = 'Security';
|
|
39
|
+
this.enumerateScope = true;
|
|
40
|
+
this.description = (0, parseRuleDescription_1.default)('jwtAlgorithmNone');
|
|
41
|
+
this.url = 'https://appland.com/docs/analysis/rules-reference.html#jwt-algorithm-none';
|
|
42
|
+
}
|
|
43
|
+
build() {
|
|
44
|
+
return new JwtAlgoritmNoneLogic();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.default = new JwtAlgoritmNone();
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Labels = void 0;
|
|
7
|
+
const parseRuleDescription_1 = __importDefault(require("./lib/parseRuleDescription"));
|
|
8
|
+
const models_1 = require("@appland/models");
|
|
9
|
+
var Labels;
|
|
10
|
+
(function (Labels) {
|
|
11
|
+
Labels["SignatureVerify"] = "jwt.signature.verify";
|
|
12
|
+
Labels["JwtDecode"] = "jwt.decode";
|
|
13
|
+
})(Labels = exports.Labels || (exports.Labels = {}));
|
|
14
|
+
// Attempt to identify and return a JWT from an array of parameters
|
|
15
|
+
function findJwt(parameters) {
|
|
16
|
+
if (!parameters)
|
|
17
|
+
return;
|
|
18
|
+
for (const param of parameters) {
|
|
19
|
+
const tokens = param.value.split('.');
|
|
20
|
+
if (tokens.length !== 3)
|
|
21
|
+
return;
|
|
22
|
+
const [header, payload, signature] = tokens;
|
|
23
|
+
return { header, payload, signature };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Check if `obj` matches the JWT by value or by reference (receiverId)
|
|
27
|
+
function matchJwt(obj, jwt, receiverId) {
|
|
28
|
+
const byValue = jwt !== undefined && obj.value.startsWith(`${jwt.header}.${jwt.payload}`);
|
|
29
|
+
const byReference = receiverId !== undefined && receiverId === obj.object_id;
|
|
30
|
+
return byValue || byReference;
|
|
31
|
+
}
|
|
32
|
+
class JwtUnverifiedSignatureLogic {
|
|
33
|
+
matcher(event) {
|
|
34
|
+
var _a, _b, _c;
|
|
35
|
+
if (event.labels.has(Labels.SignatureVerify)) {
|
|
36
|
+
// This method is marked both as decode and signature verify. It is compliant.
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let verified = false;
|
|
40
|
+
let receiverId;
|
|
41
|
+
const jwt = findJwt(event.parameters);
|
|
42
|
+
const matches = new Array();
|
|
43
|
+
// Don't track the receiver if it's static. We'll find references of the decoded JWT passed by
|
|
44
|
+
// function parameter instead.
|
|
45
|
+
if (!event.isStatic) {
|
|
46
|
+
receiverId = (_a = event.receiver) === null || _a === void 0 ? void 0 : _a.object_id;
|
|
47
|
+
}
|
|
48
|
+
for (const { event: child } of new models_1.EventNavigator(event).following()) {
|
|
49
|
+
if (!child.labels.has(Labels.SignatureVerify)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const matchesReceiver = receiverId !== undefined && receiverId === ((_b = child.receiver) === null || _b === void 0 ? void 0 : _b.object_id);
|
|
53
|
+
const matchesParameter = (_c = child.parameters) === null || _c === void 0 ? void 0 : _c.find((param) => matchJwt(param, jwt, receiverId));
|
|
54
|
+
if (matchesReceiver || matchesParameter) {
|
|
55
|
+
verified = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!verified) {
|
|
60
|
+
matches.push({
|
|
61
|
+
event,
|
|
62
|
+
message: 'JWT signature is not validated',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return matches;
|
|
66
|
+
}
|
|
67
|
+
where(event) {
|
|
68
|
+
return event.labels.has('jwt.decode');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
class JwtUnverifiedSignature {
|
|
72
|
+
constructor() {
|
|
73
|
+
this.id = 'jwt-unverified-signature';
|
|
74
|
+
this.title = 'Unverified signature';
|
|
75
|
+
this.impactDomain = 'Security';
|
|
76
|
+
this.enumerateScope = true;
|
|
77
|
+
this.description = (0, parseRuleDescription_1.default)('jwtUnverifiedSignature');
|
|
78
|
+
this.url = 'https://appland.com/docs/analysis/rules-reference.html#jwt-unverified-signature';
|
|
79
|
+
}
|
|
80
|
+
build() {
|
|
81
|
+
return new JwtUnverifiedSignatureLogic();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.default = new JwtUnverifiedSignature();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
function matches(template, value) {
|
|
4
|
+
if (template.object_id && template.object_id === value.object_id)
|
|
5
|
+
return true;
|
|
6
|
+
if (template.value && template.value === value.value)
|
|
7
|
+
return true;
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
class Matcher {
|
|
11
|
+
constructor(root, data) {
|
|
12
|
+
this.tracked = new Map();
|
|
13
|
+
for (const value of data)
|
|
14
|
+
this.add(value, root, []);
|
|
15
|
+
}
|
|
16
|
+
add(value, origin, parents) {
|
|
17
|
+
if (isPrimitive(value))
|
|
18
|
+
return;
|
|
19
|
+
this.tracked.set(value, { value, origin, parents });
|
|
20
|
+
}
|
|
21
|
+
match(value) {
|
|
22
|
+
if (isPrimitive(value))
|
|
23
|
+
return null;
|
|
24
|
+
for (const [probe, history] of this.tracked) {
|
|
25
|
+
if (matches(probe, value))
|
|
26
|
+
return history;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
matches(values) {
|
|
31
|
+
return compact(values.map(this.match.bind(this)));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function isPrimitive(value) {
|
|
35
|
+
// we don't want to record any nulls,
|
|
36
|
+
// booleans or small strings and numbers
|
|
37
|
+
return !value.value || value.value.length < 6;
|
|
38
|
+
}
|
|
39
|
+
function isNotNullOrUndefined(x) {
|
|
40
|
+
return x !== undefined && x !== null;
|
|
41
|
+
}
|
|
42
|
+
function compact(x) {
|
|
43
|
+
return x.filter(isNotNullOrUndefined);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Tracks flow of data across the execution trace, identifying all function
|
|
47
|
+
* calls which have a tracked object as its receiver or one of the parameters.
|
|
48
|
+
* Any value such a function returns will also then become tracked.
|
|
49
|
+
* The origin chain of all values is recorded, so full provenience up to
|
|
50
|
+
* the starting set can be reconstructed.
|
|
51
|
+
* @param trackedData Initial data to track.
|
|
52
|
+
* @param startEvent The root event of the analysis.
|
|
53
|
+
* @returns Events which have a tracked piece of data as an input, each
|
|
54
|
+
* associated with the list of such inputs.
|
|
55
|
+
*/
|
|
56
|
+
function analyzeDataFlow(trackedData, startEvent) {
|
|
57
|
+
const matcher = new Matcher(startEvent, trackedData);
|
|
58
|
+
const events = new Map([
|
|
59
|
+
[startEvent, matcher.matches(trackedData)],
|
|
60
|
+
]);
|
|
61
|
+
startEvent.traverse({
|
|
62
|
+
onEnter(event) {
|
|
63
|
+
const inputs = compact([...(event.parameters || []), event.receiver]);
|
|
64
|
+
const matches = matcher.matches(inputs);
|
|
65
|
+
if (matches.length === 0)
|
|
66
|
+
return;
|
|
67
|
+
events.set(event, matches);
|
|
68
|
+
},
|
|
69
|
+
onExit({ callEvent, returnValue }) {
|
|
70
|
+
if (!returnValue)
|
|
71
|
+
return;
|
|
72
|
+
const parents = events.get(callEvent);
|
|
73
|
+
if (!parents)
|
|
74
|
+
return;
|
|
75
|
+
matcher.add(returnValue, callEvent, parents);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
return events;
|
|
79
|
+
}
|
|
80
|
+
exports.default = analyzeDataFlow;
|
package/built/rules/lib/util.js
CHANGED
|
@@ -101,7 +101,7 @@ exports.parseValue = parseValue;
|
|
|
101
101
|
const isTruthy = (valueObj) => !isFalsey(valueObj);
|
|
102
102
|
exports.isTruthy = isTruthy;
|
|
103
103
|
function providesAuthentication(event, label) {
|
|
104
|
-
return event.returnValue && event.labels.has(label) && isTruthy(event.returnValue);
|
|
104
|
+
return !!event.returnValue && event.labels.has(label) && isTruthy(event.returnValue);
|
|
105
105
|
}
|
|
106
106
|
exports.providesAuthentication = providesAuthentication;
|
|
107
107
|
function ideLink(filePath, ide, eventId) {
|
|
@@ -10,18 +10,35 @@ labels:
|
|
|
10
10
|
- deserialize.unsafe
|
|
11
11
|
- deserialize.safe
|
|
12
12
|
- deserialize.sanitize
|
|
13
|
+
scope: http_server_request
|
|
13
14
|
---
|
|
14
15
|
|
|
15
16
|
Finds occurrances of deserialization in which the mechanism employed is known to be unsafe, and the
|
|
16
|
-
data
|
|
17
|
+
data comes from an untrusted source and hasn't passed through a sanitization mechanism.
|
|
17
18
|
|
|
18
19
|
### Rule logic
|
|
19
20
|
|
|
20
|
-
Finds all events labeled `deserialize.unsafe
|
|
21
|
-
|
|
21
|
+
Finds all events labeled `deserialize.unsafe` that receive tainted data (as
|
|
22
|
+
determined by object identity or string value) as an input.
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
For each of these events; checks if all the inputs have been sanitized.
|
|
25
|
+
|
|
26
|
+
Data that has been passed to a function labeled `deserialize.sanitize` is
|
|
27
|
+
assumed to be sanitized from this point onwards. Such a function could either
|
|
28
|
+
check the value is sanitized (note no verification is currently done to ensure
|
|
29
|
+
this result is checked) or return the transformed value after any necessary sanitization.
|
|
30
|
+
|
|
31
|
+
Data passed to a function labeled `deserialized.safe` is considered in all
|
|
32
|
+
functions called by it (down the callstack). Functions that first sanitize data
|
|
33
|
+
and then use an unsafe deserialization function should carry this label.
|
|
34
|
+
|
|
35
|
+
The set of tracked tainted data initially includes the HTTP message parameters
|
|
36
|
+
and is expanded to include any non-primitive (ie. longer than 5 characters)
|
|
37
|
+
observed outputs of functions that consume tainted data.
|
|
38
|
+
|
|
39
|
+
The reliability of this rule now depends on completeness of the AppMap.
|
|
40
|
+
If there is a data transformation that is not captured it's invisible to the
|
|
41
|
+
rule and will result in failure to associate it with the tracked untrusted data.
|
|
25
42
|
|
|
26
43
|
### Notes
|
|
27
44
|
|
|
@@ -30,9 +47,12 @@ that executes code shortly after deserialization.
|
|
|
30
47
|
|
|
31
48
|
### Resolution
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
Consider if the library you're using offers a safe deserialization function variant that you can
|
|
51
|
+
use instead. Using unsafe functions is only rarely needed and typically requires a good reason.
|
|
52
|
+
|
|
53
|
+
If you need to use the unsafe function, make sure you're able to handle unexpected input safely.
|
|
54
|
+
Sanitize the data thoroughly first; label the sanitization function with `deserialize.sanitize` label
|
|
55
|
+
or wrap the whole sanitization and deserialization logic in a function labeled `deserialize.safe`.
|
|
36
56
|
|
|
37
57
|
If you need to deserialize untrusted data, JSON is often a good choice as it is only capable of
|
|
38
58
|
returning ‘primitive’ types such as strings, arrays, hashes, numbers and nil. If you need to
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
rule: jwt-algorithm-none
|
|
3
|
+
name: Jwt algorithm none
|
|
4
|
+
title: JWT 'none' algorithm
|
|
5
|
+
impactDomain: Security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Finds usage of unsecured JWTs which use the `none` algorithm. When declaring this algorithm, there
|
|
9
|
+
is no signature contained within the token that may be cryptographically verified. As a result, the
|
|
10
|
+
data encoded within the token may be easily forged.
|
|
11
|
+
|
|
12
|
+
### Rule logic
|
|
13
|
+
|
|
14
|
+
Any function which encodes a new JWT will have its return value checked for presence of the `none`
|
|
15
|
+
algorithm within the token header.
|
|
16
|
+
|
|
17
|
+
### Options
|
|
18
|
+
|
|
19
|
+
None
|
|
20
|
+
|
|
21
|
+
### Examples
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
- rule: jwt-algorithm-none
|
|
25
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
rule: jwt-unverified-signature
|
|
3
|
+
name: Jwt unverified signature
|
|
4
|
+
title: Unverified signature
|
|
5
|
+
impactDomain: Security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Finds cases where a JWT is decoded but the signature is never verified. Without proper signature
|
|
9
|
+
verification, the service will unknowingly accept arbitrary token payloads from any origin.
|
|
10
|
+
|
|
11
|
+
### Rule logic
|
|
12
|
+
|
|
13
|
+
Following a function call to decode a JWT, a subsequent function call to verify the token signature
|
|
14
|
+
is expected.
|
|
15
|
+
|
|
16
|
+
### Options
|
|
17
|
+
|
|
18
|
+
None
|
|
19
|
+
|
|
20
|
+
### Examples
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
- rule: jwt-unverified-signature
|
|
24
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appland/scanner",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.70.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"bin": "built/cli.js",
|
|
6
6
|
"files": [
|
|
@@ -56,19 +56,21 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@appland/client": "^1.3.0",
|
|
59
|
-
"@appland/models": "^1.
|
|
59
|
+
"@appland/models": "^1.18.1",
|
|
60
60
|
"@appland/openapi": "1.0.2",
|
|
61
61
|
"@appland/sql-parser": "^1.5.0",
|
|
62
62
|
"@types/cli-progress": "^3.9.2",
|
|
63
63
|
"ajv": "^8.8.2",
|
|
64
64
|
"applicationinsights": "^2.1.4",
|
|
65
65
|
"async": "^3.2.3",
|
|
66
|
+
"boxen": "^5.0.1",
|
|
66
67
|
"chalk": "^4.1.2",
|
|
67
68
|
"chokidar": "applandinc/chokidar#fix/new-file-new-directory-race-on-linux",
|
|
68
69
|
"cli-progress": "^3.11.0",
|
|
69
70
|
"conf": "^10.0.2",
|
|
70
71
|
"form-data": "^4.0.0",
|
|
71
72
|
"glob": "7.2.3",
|
|
73
|
+
"inquirer": "^8.1.2",
|
|
72
74
|
"js-yaml": "^4.1.0",
|
|
73
75
|
"lru-cache": "^6.0.0",
|
|
74
76
|
"minimatch": "^3.0.4",
|