@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.
@@ -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) return res.status(blockStatus).json({
827
- error: 'Forbidden',
828
- message: 'Malicious payload detected by fortifyjs',
829
- details: { label: attack.label }
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) return res.status(blockStatus).json({
862
- error: 'Forbidden',
863
- message: 'Malicious payload detected by fortifyjs',
864
- details: { label: attack.label }
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) return res.status(blockStatus).json({
883
- error: 'Forbidden',
884
- message: 'Malicious payload detected by fortifyjs',
885
- details: { label: attack.label }
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
  }
@@ -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;
@@ -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\x0B\x0C\x0E-\x1F\x7F]/g, '');
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
+ };
@@ -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)\b/i
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: /(?:\.\.[\\\/]){2,}/
11
+ pattern: /(?:\.\.[\\\/])+/
12
12
  },
13
13
  {
14
14
  id: 'windows-backslash-traversal',
15
15
  confidence: 0.75,
16
- pattern: /(?:\.\.\\){2,}/
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){2,}/i
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){2,}/i
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|\\system32\\config)/i
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__"\s*:/
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"\s*:/
61
+ pattern: /(?:"|')?constructor(?:"|')?\s*[:=]/i
52
62
  },
53
63
  {
54
64
  id: 'proto-in-url',
@@ -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
  }