@chiranthmoger/fortifyjs 1.1.0 → 1.1.1
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 +231 -100
- package/bin/banner.js +29 -0
- package/bin/fortifyjs.js +12 -12
- package/index.d.ts +103 -3
- package/package.json +3 -2
- package/src/adapters/express.js +36 -21
- package/src/core/engine.js +63 -1
- package/src/core/normalizer.js +2 -2
- package/src/core/sinks.js +214 -0
- package/src/detectors/cmdi.js +7 -2
- package/src/detectors/open-redirect.js +10 -0
- package/src/detectors/path-traversal.js +17 -7
- package/src/detectors/prompt-injection.js +110 -0
- package/src/detectors/prototype-pollution.js +12 -2
- package/src/detectors/sqli.js +10 -0
- package/src/detectors/ssrf.js +202 -13
- package/src/detectors/template-injection.js +2 -2
- package/src/detectors/xss.js +22 -17
- package/src/index.js +34 -2
- package/src/presets.js +19 -1
- package/src/shields/csrf.js +10 -4
- package/src/shields/file-upload.js +5 -0
- package/src/shields/llm-guard.js +235 -0
- package/src/shields/rate-limiter.js +53 -11
- package/src/shields/sanitizer.js +113 -0
- package/src/shields/store.js +97 -0
- package/src/detectors/sqli.js.bak +0 -446
package/src/adapters/express.js
CHANGED
|
@@ -823,11 +823,16 @@ function expressMiddleware(options = {}) {
|
|
|
823
823
|
|
|
824
824
|
if (schemaResult) {
|
|
825
825
|
const attack = reportDetection(schemaResult.payload, schemaResult.detection);
|
|
826
|
-
if (!dryRun)
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
826
|
+
if (!dryRun) {
|
|
827
|
+
if (typeof options.onBlocked === 'function') {
|
|
828
|
+
return options.onBlocked(req, res, attack);
|
|
829
|
+
}
|
|
830
|
+
return res.status(blockStatus).json({
|
|
831
|
+
error: 'Forbidden',
|
|
832
|
+
message: 'Malicious payload detected by fortifyjs',
|
|
833
|
+
details: { label: attack.label }
|
|
834
|
+
});
|
|
835
|
+
}
|
|
831
836
|
}
|
|
832
837
|
|
|
833
838
|
const sources = [];
|
|
@@ -858,11 +863,16 @@ function expressMiddleware(options = {}) {
|
|
|
858
863
|
reason: 'source_read_failed',
|
|
859
864
|
matches: [{ id: 'source-read-failed', label: 'dos', confidence: 1 }]
|
|
860
865
|
});
|
|
861
|
-
if (!dryRun)
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
+
if (!dryRun) {
|
|
867
|
+
if (typeof options.onBlocked === 'function') {
|
|
868
|
+
return options.onBlocked(req, res, attack);
|
|
869
|
+
}
|
|
870
|
+
return res.status(blockStatus).json({
|
|
871
|
+
error: 'Forbidden',
|
|
872
|
+
message: 'Malicious payload detected by fortifyjs',
|
|
873
|
+
details: { label: attack.label }
|
|
874
|
+
});
|
|
875
|
+
}
|
|
866
876
|
}
|
|
867
877
|
|
|
868
878
|
for (const [sourceName, source] of sources) {
|
|
@@ -871,19 +881,24 @@ function expressMiddleware(options = {}) {
|
|
|
871
881
|
|
|
872
882
|
let attack = false;
|
|
873
883
|
if (Buffer.isBuffer(source)) {
|
|
874
|
-
attack = await scanString(source.toString('utf8'), sourceName);
|
|
875
|
-
} else if (typeof source === 'string') {
|
|
876
|
-
attack = await scanString(source, sourceName);
|
|
877
|
-
} else if (typeof source === 'object') {
|
|
878
|
-
attack = await deepScan(source, sourceName);
|
|
879
|
-
}
|
|
884
|
+
attack = await scanString(source.toString('utf8'), sourceName);
|
|
885
|
+
} else if (typeof source === 'string') {
|
|
886
|
+
attack = await scanString(source, sourceName);
|
|
887
|
+
} else if (typeof source === 'object') {
|
|
888
|
+
attack = await deepScan(source, sourceName);
|
|
889
|
+
}
|
|
880
890
|
|
|
881
891
|
if (attack) {
|
|
882
|
-
if (!dryRun)
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
892
|
+
if (!dryRun) {
|
|
893
|
+
if (typeof options.onBlocked === 'function') {
|
|
894
|
+
return options.onBlocked(req, res, attack);
|
|
895
|
+
}
|
|
896
|
+
return res.status(blockStatus).json({
|
|
897
|
+
error: 'Forbidden',
|
|
898
|
+
message: 'Malicious payload detected by fortifyjs',
|
|
899
|
+
details: { label: attack.label }
|
|
900
|
+
});
|
|
901
|
+
}
|
|
887
902
|
}
|
|
888
903
|
scannedSources.add(sourceName);
|
|
889
904
|
}
|
package/src/core/engine.js
CHANGED
|
@@ -10,6 +10,7 @@ const nosqliDetector = require('../detectors/nosqli');
|
|
|
10
10
|
const cmdiDetector = require('../detectors/cmdi');
|
|
11
11
|
const pathTraversalDetector = require('../detectors/path-traversal');
|
|
12
12
|
const ssrfDetector = require('../detectors/ssrf');
|
|
13
|
+
const { isPrivateIpv4Number } = require('../detectors/ssrf');
|
|
13
14
|
const xxeDetector = require('../detectors/xxe');
|
|
14
15
|
const prototypePollutionDetector = require('../detectors/prototype-pollution');
|
|
15
16
|
const hppDetector = require('../detectors/hpp');
|
|
@@ -18,6 +19,63 @@ const crlfDetector = require('../detectors/crlf');
|
|
|
18
19
|
const templateInjectionDetector = require('../detectors/template-injection');
|
|
19
20
|
const ldapDetector = require('../detectors/ldap');
|
|
20
21
|
const graphqlDetector = require('../detectors/graphql');
|
|
22
|
+
const promptInjectionDetector = require('../detectors/prompt-injection');
|
|
23
|
+
|
|
24
|
+
const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
25
|
+
const EMAIL_REGEX = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
|
|
26
|
+
const ATOMIC_IDENTIFIER_REGEX = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
27
|
+
const NUMERIC_REGEX = /^-?\d+(?:\.\d+)?$/;
|
|
28
|
+
|
|
29
|
+
const DANGEROUS_ATOMIC_IDENTIFIERS = new Set([
|
|
30
|
+
'SELECT', 'UNION', 'DROP', 'DELETE', 'INSERT', 'UPDATE', 'EXEC',
|
|
31
|
+
'TRUNCATE', 'ALTER', 'CREATE', 'MERGE', 'GRANT', 'REVOKE',
|
|
32
|
+
'__PROTO__', 'CONSTRUCTOR', 'PROTOTYPE',
|
|
33
|
+
'__SCHEMA', '__TYPE', '__TYPENAME',
|
|
34
|
+
'INFORMATION_SCHEMA', 'SQLITE_MASTER', 'SYSOBJECTS',
|
|
35
|
+
'LOCALHOST', 'SCRIPT', 'JAVASCRIPT', 'ALERT', 'EVAL'
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function isTriviallySafe(payload) {
|
|
39
|
+
if (typeof payload === 'number' || typeof payload === 'boolean') return true;
|
|
40
|
+
if (typeof payload !== 'string') return false;
|
|
41
|
+
if (payload.length === 0) return true;
|
|
42
|
+
if (payload.length > 128) return false;
|
|
43
|
+
|
|
44
|
+
// Single word identifier / slug
|
|
45
|
+
if (ATOMIC_IDENTIFIER_REGEX.test(payload)) {
|
|
46
|
+
const upper = payload.toUpperCase();
|
|
47
|
+
if (DANGEROUS_ATOMIC_IDENTIFIERS.has(upper)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Check if hex integer (e.g. 0x7f000001)
|
|
51
|
+
if (/^0x[0-9a-fA-F]+$/i.test(payload)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// Check if decimal IP representation in private range
|
|
55
|
+
if (/^\d{8,10}$/.test(payload)) {
|
|
56
|
+
const num = Number(payload);
|
|
57
|
+
if (Number.isFinite(num) && isPrivateIpv4Number(num)) return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pure numeric
|
|
63
|
+
if (NUMERIC_REGEX.test(payload)) {
|
|
64
|
+
if (/^\d{8,10}$/.test(payload)) {
|
|
65
|
+
const num = Number(payload);
|
|
66
|
+
if (Number.isFinite(num) && isPrivateIpv4Number(num)) return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Standard UUID
|
|
72
|
+
if (payload.length === 36 && UUID_REGEX.test(payload)) return true;
|
|
73
|
+
|
|
74
|
+
// Standard Email
|
|
75
|
+
if (payload.includes('@') && EMAIL_REGEX.test(payload)) return true;
|
|
76
|
+
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
21
79
|
|
|
22
80
|
function classifyInputType(payload) {
|
|
23
81
|
const str = String(payload).trim();
|
|
@@ -52,7 +110,8 @@ class DetectionEngine {
|
|
|
52
110
|
crlfDetector,
|
|
53
111
|
templateInjectionDetector,
|
|
54
112
|
ldapDetector,
|
|
55
|
-
graphqlDetector
|
|
113
|
+
graphqlDetector,
|
|
114
|
+
promptInjectionDetector
|
|
56
115
|
];
|
|
57
116
|
this.behavioralAnalyzer = new BehavioralAnalyzer(options.behavioral || {});
|
|
58
117
|
this.whitelist = new Whitelist();
|
|
@@ -73,6 +132,9 @@ class DetectionEngine {
|
|
|
73
132
|
if (this.whitelist.isWhitelisted(payload)) {
|
|
74
133
|
return { label: 'benign', confidence: 0, whitelisted: true };
|
|
75
134
|
}
|
|
135
|
+
if (this.detectors.length === 15 && this.options.fastPath !== false && isTriviallySafe(payload) && !context.source && this.options.mode !== 'query') {
|
|
136
|
+
return { label: 'benign', confidence: 0, scores: {}, matches: [], fastPath: true };
|
|
137
|
+
}
|
|
76
138
|
const variants = Normalizer.payloadVariants(payload, this.options);
|
|
77
139
|
let allMatches = [];
|
|
78
140
|
let maxConfidence = 0;
|
package/src/core/normalizer.js
CHANGED
|
@@ -68,9 +68,9 @@ class Normalizer {
|
|
|
68
68
|
.replace(/\\x([0-9a-fA-F]{2})/g, decodeCodePoint)
|
|
69
69
|
.replace(/&#x([0-9a-fA-F]+);?|&#(\d+);?/g, decodeEntity)
|
|
70
70
|
.replace(NAMED_ENTITY_PATTERN, (match, name) => NAMED_ENTITIES[name.toLowerCase()] ?? match)
|
|
71
|
-
.replace(/[\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/g, ' ')
|
|
71
|
+
.replace(/[\x0B\x0C\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/g, ' ')
|
|
72
72
|
.replace(/[\u200b-\u200d\ufeff]/g, '')
|
|
73
|
-
.replace(/[\x00-\x08\
|
|
73
|
+
.replace(/[\x00-\x08\x0E-\x1F\x7F]/g, '');
|
|
74
74
|
try {
|
|
75
75
|
normalized = normalized.normalize('NFKC');
|
|
76
76
|
} catch (e) {}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { DetectionEngine } = require('./engine');
|
|
5
|
+
const { isPrivateIP } = require('../detectors/ssrf');
|
|
6
|
+
const { scanPrompt, assertSafePrompt, FortifyPromptError } = require('../shields/llm-guard');
|
|
7
|
+
|
|
8
|
+
class FortifySinkError extends Error {
|
|
9
|
+
constructor(message, sinkType, result = null) {
|
|
10
|
+
super(`FortifyJS Sink Violation [${sinkType}]: ${message}`);
|
|
11
|
+
this.name = 'FortifySinkError';
|
|
12
|
+
this.sinkType = sinkType;
|
|
13
|
+
this.status = 403;
|
|
14
|
+
this.code = `FORTIFY_SINK_${sinkType.toUpperCase()}`;
|
|
15
|
+
this.result = result;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Asserts that an OS command string is safe from command injection.
|
|
21
|
+
* @param {string} command
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @returns {Object} detection result
|
|
24
|
+
*/
|
|
25
|
+
function assertSafeCommand(command, options = {}) {
|
|
26
|
+
if (typeof command !== 'string') {
|
|
27
|
+
throw new TypeError('command must be a string');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const detector = options.detector || new DetectionEngine({ mode: 'command' });
|
|
31
|
+
const result = detector.detect(command);
|
|
32
|
+
const threshold = options.threshold !== undefined ? options.threshold : 0.45;
|
|
33
|
+
|
|
34
|
+
if (result.label === 'cmdi' && result.confidence >= threshold) {
|
|
35
|
+
throw new FortifySinkError('Unsafe OS command injection pattern detected', 'COMMAND', result);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Asserts that a file path is safe and contained within an allowed root directory.
|
|
43
|
+
* @param {string} userPath
|
|
44
|
+
* @param {Object} options { rootDir: string }
|
|
45
|
+
* @returns {string} normalized resolved safe path
|
|
46
|
+
*/
|
|
47
|
+
function assertSafePath(userPath, options = {}) {
|
|
48
|
+
if (typeof userPath !== 'string') {
|
|
49
|
+
throw new TypeError('path must be a string');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const detector = options.detector || new DetectionEngine();
|
|
53
|
+
const result = detector.detect(userPath, { source: 'filename' });
|
|
54
|
+
const threshold = options.threshold !== undefined ? options.threshold : 0.45;
|
|
55
|
+
|
|
56
|
+
// Check traversal patterns and anomaly detections
|
|
57
|
+
if ((result.label === 'path-traversal' || result.label === 'anomaly') && result.confidence >= threshold) {
|
|
58
|
+
throw new FortifySinkError('Path traversal sequence detected in file path', 'PATH', result);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for raw traversal patterns (including single ..)
|
|
62
|
+
if (/(?:^|[\\\/])\.\.(?:[\\\/]|$)/.test(userPath) || userPath.includes('..')) {
|
|
63
|
+
throw new FortifySinkError('Path traversal sequence (..) detected in file path', 'PATH');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for null-byte injection
|
|
67
|
+
if (userPath.includes('\0') || userPath.includes('%00')) {
|
|
68
|
+
throw new FortifySinkError('Null byte injection detected in path', 'PATH');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check root containment if rootDir is provided
|
|
72
|
+
if (options.rootDir) {
|
|
73
|
+
const resolvedRoot = path.resolve(options.rootDir);
|
|
74
|
+
const resolvedTarget = path.resolve(resolvedRoot, userPath);
|
|
75
|
+
if (!resolvedTarget.startsWith(resolvedRoot + path.sep) && resolvedTarget !== resolvedRoot) {
|
|
76
|
+
throw new FortifySinkError(`Path '${userPath}' escapes root directory '${options.rootDir}'`, 'PATH');
|
|
77
|
+
}
|
|
78
|
+
return resolvedTarget;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return userPath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Asserts that a URL is safe from SSRF and does not target private IP subnets or cloud metadata.
|
|
86
|
+
* @param {string} targetUrl
|
|
87
|
+
* @param {Object} options { allowPrivate: boolean, allowedProtocols: string[] }
|
|
88
|
+
* @returns {Object} parsed URL object
|
|
89
|
+
*/
|
|
90
|
+
function assertSafeUrl(targetUrl, options = {}) {
|
|
91
|
+
if (typeof targetUrl !== 'string') {
|
|
92
|
+
throw new TypeError('url must be a string');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const allowedProtocols = options.allowedProtocols || ['http:', 'https:'];
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = new URL(targetUrl);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
throw new FortifySinkError(`Invalid URL format: ${targetUrl}`, 'URL');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
104
|
+
throw new FortifySinkError(`Dangerous or disallowed URL protocol '${parsed.protocol}'`, 'URL');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (options.allowPrivate !== true) {
|
|
108
|
+
const hostname = parsed.hostname;
|
|
109
|
+
if (isPrivateIP(hostname)) {
|
|
110
|
+
throw new FortifySinkError(`Blocked access to private network / cloud metadata address: ${hostname}`, 'URL');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const detector = options.detector || new DetectionEngine();
|
|
115
|
+
const result = detector.detect(targetUrl);
|
|
116
|
+
if (result.label === 'ssrf' && result.confidence >= (options.threshold || 0.5)) {
|
|
117
|
+
throw new FortifySinkError(`SSRF attack pattern detected in target URL`, 'URL', result);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Asserts that a MongoDB / NoSQL query object does not contain operator injection ($where, $gt, etc.).
|
|
125
|
+
* @param {Object} query
|
|
126
|
+
* @param {Object} options
|
|
127
|
+
* @returns {Object} query
|
|
128
|
+
*/
|
|
129
|
+
function assertSafeNoSql(query, options = {}) {
|
|
130
|
+
if (!query || typeof query !== 'object') return query;
|
|
131
|
+
|
|
132
|
+
const forbiddenOperators = options.forbiddenOperators || ['$where', '$regex', '$expr', '$function', '$accumulator'];
|
|
133
|
+
const visited = new WeakSet();
|
|
134
|
+
|
|
135
|
+
function inspectNode(node) {
|
|
136
|
+
if (!node || typeof node !== 'object') return;
|
|
137
|
+
if (visited.has(node)) return;
|
|
138
|
+
visited.add(node);
|
|
139
|
+
|
|
140
|
+
if (Array.isArray(node)) {
|
|
141
|
+
for (const item of node) inspectNode(item);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const key of Object.keys(node)) {
|
|
146
|
+
if (forbiddenOperators.includes(key)) {
|
|
147
|
+
throw new FortifySinkError(`Forbidden NoSQL query operator '${key}' detected`, 'NOSQL');
|
|
148
|
+
}
|
|
149
|
+
if (typeof key === 'string' && key.startsWith('$') && options.disallowAllOperators) {
|
|
150
|
+
throw new FortifySinkError(`Disallowed query operator '${key}' detected`, 'NOSQL');
|
|
151
|
+
}
|
|
152
|
+
inspectNode(node[key]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
inspectNode(query);
|
|
157
|
+
return query;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Asserts that a redirect destination is safe from Open Redirect vulnerabilities.
|
|
162
|
+
* @param {string} destination
|
|
163
|
+
* @param {Object} options { allowedHosts: string[], allowRelative: boolean }
|
|
164
|
+
* @returns {string} destination
|
|
165
|
+
*/
|
|
166
|
+
function assertSafeRedirect(destination, options = {}) {
|
|
167
|
+
if (typeof destination !== 'string') {
|
|
168
|
+
throw new TypeError('destination must be a string');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const allowRelative = options.allowRelative !== false;
|
|
172
|
+
const allowedHosts = (options.allowedHosts || []).map(h => h.toLowerCase());
|
|
173
|
+
|
|
174
|
+
// Check for protocol-relative bypass (//evil.com)
|
|
175
|
+
if (destination.startsWith('//') || destination.startsWith('\\\\')) {
|
|
176
|
+
throw new FortifySinkError('Protocol-relative URL redirect forbidden', 'REDIRECT');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Relative paths
|
|
180
|
+
if (destination.startsWith('/') && !destination.startsWith('/\\')) {
|
|
181
|
+
if (allowRelative) return destination;
|
|
182
|
+
throw new FortifySinkError('Relative redirect forbidden by policy', 'REDIRECT');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Absolute URL validation
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = new URL(destination);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
throw new FortifySinkError(`Invalid redirect destination: ${destination}`, 'REDIRECT');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
194
|
+
throw new FortifySinkError(`Disallowed redirect protocol: ${parsed.protocol}`, 'REDIRECT');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (allowedHosts.length > 0 && !allowedHosts.includes(parsed.hostname.toLowerCase())) {
|
|
198
|
+
throw new FortifySinkError(`Redirect domain '${parsed.hostname}' is not in allowedHosts list`, 'REDIRECT');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return destination;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
FortifySinkError,
|
|
206
|
+
FortifyPromptError,
|
|
207
|
+
assertSafeCommand,
|
|
208
|
+
assertSafePath,
|
|
209
|
+
assertSafeUrl,
|
|
210
|
+
assertSafeNoSql,
|
|
211
|
+
assertSafeRedirect,
|
|
212
|
+
assertSafePrompt,
|
|
213
|
+
scanPrompt
|
|
214
|
+
};
|
package/src/detectors/cmdi.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const DANGEROUS_COMMANDS = 'rm|cat|wget|curl|nc|bash|sh|cmd|powershell|python|perl|ruby|node|php';
|
|
3
|
+
const DANGEROUS_COMMANDS = 'rm|cat|wget|curl|nc|bash|sh|cmd|powershell|python|perl|ruby|node|php|id|whoami|ls|uname|hostname|awk|sed|find|socat|ncat|xargs|dd|env|tee';
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
6
|
name: 'cmdi',
|
|
@@ -75,7 +75,12 @@ module.exports = {
|
|
|
75
75
|
{
|
|
76
76
|
id: 'pipe-operator-command',
|
|
77
77
|
confidence: 0.80,
|
|
78
|
-
pattern: /\|\s*(?:ls|cat|whoami|id|pwd|dir|type)
|
|
78
|
+
pattern: /\|\s*(?:ls|cat|whoami|id|pwd|dir|type|nc|ncat|socat|tee)(?:\s|$)/i
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'pipe-to-dangerous-binary',
|
|
82
|
+
confidence: 0.75,
|
|
83
|
+
pattern: new RegExp(`\\|\\s*(?:${DANGEROUS_COMMANDS})(?:\\s|$)`, 'i')
|
|
79
84
|
}
|
|
80
85
|
];
|
|
81
86
|
}
|
|
@@ -36,6 +36,16 @@ module.exports = {
|
|
|
36
36
|
id: 'javascript-uri-redirect',
|
|
37
37
|
confidence: 0.85,
|
|
38
38
|
pattern: new RegExp(`(?:\\?|&)(?:${REDIRECT_PARAMS})=javascript:`, 'i')
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'bare-protocol-relative-url',
|
|
42
|
+
confidence: 0.70,
|
|
43
|
+
pattern: /^\/\/[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'bare-external-redirect',
|
|
47
|
+
confidence: 0.65,
|
|
48
|
+
pattern: /^https?:\/\/[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/
|
|
39
49
|
}
|
|
40
50
|
];
|
|
41
51
|
}
|
|
@@ -8,32 +8,42 @@ module.exports = {
|
|
|
8
8
|
{
|
|
9
9
|
id: 'dot-dot-slash',
|
|
10
10
|
confidence: 0.75,
|
|
11
|
-
pattern: /(?:\.\.[\\\/])
|
|
11
|
+
pattern: /(?:\.\.[\\\/])+/
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
id: 'windows-backslash-traversal',
|
|
15
15
|
confidence: 0.75,
|
|
16
|
-
pattern: /(?:\.\.\\)
|
|
16
|
+
pattern: /(?:\.\.\\)+/
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
id: 'url-encoded-traversal',
|
|
20
20
|
confidence: 0.80,
|
|
21
|
-
pattern: /(?:%2e%2e%2f|%252e%252e%252f|%2e%2e%5c|%252e%252e%255c)
|
|
21
|
+
pattern: /(?:%2e%2e%2f|%252e%252e%252f|%2e%2e%5c|%252e%252e%255c)+/i
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
id: 'overlong-utf8-traversal',
|
|
25
25
|
confidence: 0.85,
|
|
26
|
-
pattern: /(?:%c0%ae%c0%ae%c0%af|%e0%40%ae%e0%40%ae%e0%40%af)
|
|
26
|
+
pattern: /(?:%c0%ae%c0%ae%c0%af|%e0%40%ae%e0%40%ae%e0%40%af)+/i
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
id: 'double-dot-slash',
|
|
30
30
|
confidence: 0.75,
|
|
31
|
-
pattern: /(
|
|
31
|
+
pattern: /(?:\.{2,}[\\\/]+)+/
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
id: 'null-byte-injection',
|
|
35
35
|
confidence: 0.85,
|
|
36
|
-
pattern: /\x00/
|
|
36
|
+
pattern: /\x00|%00/i
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'ntfs-alternate-data-stream',
|
|
40
|
+
confidence: 0.85,
|
|
41
|
+
pattern: /::\$DATA\b/i
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'windows-unc-path',
|
|
45
|
+
confidence: 0.80,
|
|
46
|
+
pattern: /(?:^|[\\\/])\\\\\?\\/
|
|
37
47
|
},
|
|
38
48
|
{
|
|
39
49
|
id: 'sensitive-unix-file',
|
|
@@ -43,7 +53,7 @@ module.exports = {
|
|
|
43
53
|
{
|
|
44
54
|
id: 'sensitive-windows-file',
|
|
45
55
|
confidence: 0.80,
|
|
46
|
-
pattern: /(?:\\SAM|\\boot\.ini|\\win\.ini
|
|
56
|
+
pattern: /(?:\\SAM|\\boot\.ini|\\win\.ini|[\\\/]system32(?:[\\\/]|$))/i
|
|
47
57
|
},
|
|
48
58
|
{
|
|
49
59
|
id: 'dotfile-access',
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI & LLM Prompt Injection Detector for FortifyJS
|
|
5
|
+
* Detects direct instruction overrides, jailbreaks (DAN, Dev Mode),
|
|
6
|
+
* system prompt exfiltration, delimiter hijacking, and adversarial roleplay.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function isAdversarialPromptStructure(text) {
|
|
10
|
+
if (!text || typeof text !== 'string') return false;
|
|
11
|
+
const lower = text.toLowerCase();
|
|
12
|
+
// Collapsed version for punctuation-separated token obfuscation (i.g.n.o.r.e -> ignore)
|
|
13
|
+
const collapsed = lower.replace(/(?<=[a-z0-9])[.\-_/|\\]+(?=[a-z0-9])/gi, '');
|
|
14
|
+
|
|
15
|
+
const checkText = (t) => {
|
|
16
|
+
// English & Multilingual overrides
|
|
17
|
+
const hasOverride = /(?:ignore|disregard|forget|bypass|override|drop|ignora|ignorez|ignoriere|игнорируй|忽略)\s+(?:(?:all|your|the|todas\s+las|toutes\s+les|alle|все|之前的所有)\s+)*(?:previous|prior|above|preceding|system|initial|core|first|earlier|anteriores|précédentes|vorherigen|предыдущие|之前)?\s*(?:instructions|prompts|rules|guidelines|directives|constraints|instrucciones|anweisungen|инструкции|指令|提示词)/i.test(t);
|
|
18
|
+
const hasNewDirective = /(?:now\s+you|you\s+are\s+now|instead|reply\s+with|act\s+as|output|tell\s+me|print|ahora|maintenant|jetzt|сейчас|现在|输出)/i.test(t);
|
|
19
|
+
if (hasOverride && hasNewDirective) return true;
|
|
20
|
+
|
|
21
|
+
// Multilingual & structural system exfiltration
|
|
22
|
+
const hasExfiltrationAction = /(?:repeat|output|print|show|dump|reveal|leak|display|echo|verbatim|write\s+out|muestra|affichez|zeige|покажи|输出|显示)\s+(?:the\s+|your\s+|el\s+|les\s+|die\s+|весь\s+)?(?:entire\s+|full\s+|exact\s+|original\s+|iniciales\s+|initial\s+)?(?:system\s+prompt|initial\s+instructions|system\s+instructions|developer\s+message|secret\s+instructions|hidden\s+prompt|prompt\s+del\s+sistema|instructions\s+initiales|системный\s+промпт|系统提示词|系统指令)/i.test(t);
|
|
23
|
+
if (hasExfiltrationAction) return true;
|
|
24
|
+
|
|
25
|
+
// Encoding exfiltration
|
|
26
|
+
const hasEncodedExfiltration = /(?:encode|convert|translate|format)\s+(?:the\s+|your\s+)?(?:system\s+prompt|initial\s+instructions|secret\s+prompt)\s+(?:into|as|in|to)\s+(?:base64|hex|rot13|json|binary|morse|url)/i.test(t);
|
|
27
|
+
if (hasEncodedExfiltration) return true;
|
|
28
|
+
|
|
29
|
+
return false;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return checkText(lower) || checkText(collapsed);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
name: 'prompt-injection',
|
|
37
|
+
label: 'prompt-injection',
|
|
38
|
+
|
|
39
|
+
isAdversarialPromptStructure,
|
|
40
|
+
|
|
41
|
+
getSignals() {
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
id: 'prompt-instruction-override',
|
|
45
|
+
confidence: 0.90,
|
|
46
|
+
pattern: /(?:ignore|disregard|forget|bypass|override|drop|ignora|ignorez|ignoriere|игнорируй|忽略)\s+(?:(?:all|your|the|todas\s+las|toutes\s+les|alle|все|之前的所有)\s+)*(?:previous|prior|above|preceding|system|initial|core|first|earlier|anteriores|précédentes|vorherigen|предыдущие|之前)?\s*(?:instructions|prompts|rules|guidelines|directives|constraints|instrucciones|anweisungen|инструкции|指令|提示词)/i
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'prompt-adversarial-structure',
|
|
50
|
+
confidence: 0.85,
|
|
51
|
+
test: isAdversarialPromptStructure
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'prompt-multilingual-jailbreak',
|
|
55
|
+
confidence: 0.90,
|
|
56
|
+
pattern: /(?:ignora\s+todas\s+las\s+instrucciones|ignor(?:ez|er)\s+toutes\s+les\s+instructions|ignoriere\s+alle\s+anweisungen|忽略之前的所有指令|игнорируй\s+все\s+предыдущие\s+инструкции)/i
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'prompt-jailbreak-persona',
|
|
60
|
+
confidence: 0.90,
|
|
61
|
+
pattern: /\b(?:DAN\s+mode|do\s+anything\s+now|developer\s+mode\s+enabled|jailbreak(?:ed)?\s+mode|unfiltered\s+mode|never\s+refuse\s+any\s+request|stay\s+in\s+character\s+as\s+(?:evil|unrestricted|unfiltered)|unrestricted\s+AI)\b/i
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'prompt-system-exfiltration',
|
|
65
|
+
confidence: 0.90,
|
|
66
|
+
pattern: /(?:output|print|repeat|show|dump|reveal|leak|display|muestra|affichez|zeige|покажи|输出|显示)\s+(?:your\s+|the\s+|el\s+|les\s+|die\s+)?(?:exact\s+|entire\s+|full\s+|complete\s+)?(?:system\s+prompt|initial\s+instructions|developer\s+prompt|hidden\s+instructions|prompt\s+del\s+sistema|instructions\s+initiales|系统提示词)/i
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'prompt-control-tokens',
|
|
70
|
+
confidence: 0.95,
|
|
71
|
+
pattern: /(?:<\|(?:im_start|im_end|endoftext|system|user|assistant|fim_prefix|fim_suffix)\||---BEGIN\s+(?:SYSTEM|PROMPT|INSTRUCTIONS)---)/i
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'prompt-delimiter-hijack',
|
|
75
|
+
confidence: 0.85,
|
|
76
|
+
pattern: /(?:\[\s*(?:SYSTEM|SYS|INST|SYSTEM_PROMPT|DEVELOPER_INSTRUCTIONS)\s*\]|<\s*(?:system|instruction|developer)\s*>|###\s*(?:System|Instruction|Assistant|Human)\s*:|<<SYS>>|<\/SYS>)/i
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'prompt-safety-bypass-directive',
|
|
80
|
+
confidence: 0.85,
|
|
81
|
+
pattern: /(?:bypass|disable|turn\s+off|remove|ignore)\s+(?:your\s+)?(?:content\s+filters?|safety\s+filters?|moderation|guardrails|ethical\s+guidelines|safety\s+protocols?)/i
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'prompt-markdown-exfiltration',
|
|
85
|
+
confidence: 0.80,
|
|
86
|
+
pattern: /!\[.*?\]\(https?:\/\/[^\s)]+\?[^\s)]*(?:prompt|token|key|secret|system|data)=/i
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'prompt-format-coercion',
|
|
90
|
+
confidence: 0.85,
|
|
91
|
+
pattern: /(?:you\s+must\s+respond\s+only\s+with|respond\s+in\s+json\s+with\s+the\s+fields?\s*:\s*["']?system_prompt["']?)/i
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'prompt-translation-smuggle',
|
|
95
|
+
confidence: 0.85,
|
|
96
|
+
pattern: /(?:translate\s+(?:the\s+following|this)\s+(?:from\s+[a-z]+\s+to\s+[a-z]+|into\s+[a-z]+)\s+and\s+execute|decode\s+(?:base64|rot13|hex)\s+and\s+follow\s+instructions)/i
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'prompt-hypothetical-override',
|
|
100
|
+
confidence: 0.85,
|
|
101
|
+
pattern: /(?:in\s+a\s+hypothetical\s+scenario\s+where\s+safety\s+rules\s+do\s+not\s+apply|for\s+educational\s+and\s+research\s+purposes\s+only,\s+bypass\s+all\s+guidelines)/i
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'prompt-token-split-smuggle',
|
|
105
|
+
confidence: 0.85,
|
|
106
|
+
pattern: /(?:I\s*G\s*N\s*O\s*R\s*E\s+P\s*R\s*E\s*V\s*I\s*O\s*U\s*S|D\s*A\s*N\s+M\s*O\s*D\s*E)/i
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
};
|
|
@@ -35,10 +35,20 @@ module.exports = {
|
|
|
35
35
|
|
|
36
36
|
getSignals() {
|
|
37
37
|
return [
|
|
38
|
+
{
|
|
39
|
+
id: 'proto-key-exact',
|
|
40
|
+
confidence: 0.90,
|
|
41
|
+
pattern: /^(?:__proto__|prototype)$/i
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'constructor-key-exact',
|
|
45
|
+
confidence: 0.70,
|
|
46
|
+
pattern: /^constructor$/i
|
|
47
|
+
},
|
|
38
48
|
{
|
|
39
49
|
id: 'proto-key',
|
|
40
50
|
confidence: 0.90,
|
|
41
|
-
pattern: /"__proto__"
|
|
51
|
+
pattern: /(?:"|')?__proto__(?:"|')?\s*[:=]/i
|
|
42
52
|
},
|
|
43
53
|
{
|
|
44
54
|
id: 'constructor-prototype',
|
|
@@ -48,7 +58,7 @@ module.exports = {
|
|
|
48
58
|
{
|
|
49
59
|
id: 'constructor-key',
|
|
50
60
|
confidence: 0.70,
|
|
51
|
-
pattern: /"constructor"
|
|
61
|
+
pattern: /(?:"|')?constructor(?:"|')?\s*[:=]/i
|
|
52
62
|
},
|
|
53
63
|
{
|
|
54
64
|
id: 'proto-in-url',
|
package/src/detectors/sqli.js
CHANGED
|
@@ -441,6 +441,16 @@ module.exports = {
|
|
|
441
441
|
id: 'sql-structural-metadata-query',
|
|
442
442
|
confidence: 0.65,
|
|
443
443
|
test: hasStructuralSqlMetadataQuery
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: 'case-when-conditional',
|
|
447
|
+
confidence: 0.70,
|
|
448
|
+
pattern: /\bCASE\s+WHEN\s+\(?\s*\d+\s*[=<>!]+\s*\d+\s*\)?\s+THEN\b/i
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: 'select-case-expression',
|
|
452
|
+
confidence: 0.70,
|
|
453
|
+
pattern: /\(\s*SELECT\s+CASE\s+WHEN\b/i
|
|
444
454
|
}
|
|
445
455
|
];
|
|
446
456
|
}
|