Skip to content

Commit fa35ba4

Browse files
author
jovanSAPFIONEER
committed
fix(security): harden justification scoring against prompt injection (v3.2.1)
- Replace simplistic keyword matching in score_justification() with multi-layered defense: injection pattern detection (16 patterns), keyword-stuffing detection, repetition/padding detection, length cap, minimum word count, and structural coherence scoring - Add detect_injection() function to catch prompt-injection attempts - Fix test-security.ts gateway audit integrity test by isolating log files - All 315 tests passing pristine (0 failures)
1 parent 12bc86e commit fa35ba4

File tree

7 files changed

+162
-31
lines changed

7 files changed

+162
-31
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ All notable changes to Network-AI will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.2.1] - 2026-02-17
9+
10+
### Security
11+
- **Hardened `score_justification()` in `check_permission.py`** -- Fixed prompt-injection bypass vulnerability flagged by ClawHub scanner; simplistic keyword matching replaced with multi-layered defense
12+
- **Added `detect_injection()` function** -- 16 regex patterns detect prompt-injection attempts (ignore previous, override policy, bypass security, admin mode, sudo, jailbreak, etc.)
13+
- **Keyword-stuffing detection** -- Penalizes justifications where >50% of words are scoring keywords
14+
- **Repetition/padding detection** -- Rejects justifications with <40% unique words
15+
- **Maximum length cap (500 chars)** -- Prevents obfuscation in excessively long justifications
16+
- **Minimum word count (3)** -- Rejects trivially short justifications
17+
- **Structural coherence scoring** -- Requires verb + noun-object structure for full score; prevents keyword-only strings from scoring high
18+
19+
### Fixed
20+
- **Security test isolation** -- Gateway audit integrity test (Test 7) now uses isolated log file, preventing cross-run HMAC signature mismatches that caused false failures
21+
- **All 315 tests now pass pristine** -- 0 failures across all 4 suites
22+
823
## [3.2.0] - 2026-02-17
924

1025
### Added -- Phase 3: Priority & Preemption

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**The plug-and-play AI agent orchestrator for TypeScript/Node.js -- connect 12 agent frameworks with zero glue code**
44

5-
[![Release](https://img.shields.io/badge/release-v3.2.0-blue.svg)](https://github.com/jovanSAPFIONEER/Network-AI/releases)
5+
[![Release](https://img.shields.io/badge/release-v3.2.1-blue.svg)](https://github.com/jovanSAPFIONEER/Network-AI/releases)
66
[![ClawHub](https://img.shields.io/badge/ClawHub-network--ai-orange.svg)](https://clawhub.ai/skills/network-ai)
77
[![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org)
88
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6.svg)](https://typescriptlang.org)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "network-ai",
3-
"version": "3.2.0",
3+
"version": "3.2.1",
44
"description": "AI agent orchestration framework for TypeScript/Node.js - plug-and-play multi-agent coordination with 12 frameworks (LangChain, AutoGen, CrewAI, OpenAI Assistants, LlamaIndex, Semantic Kernel, Haystack, DSPy, Agno, MCP, OpenClaw). Built-in security, swarm intelligence, and agentic workflow patterns.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

scripts/check_permission.py

Lines changed: 125 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,31 +61,135 @@ def ensure_data_dir():
6161
return data_dir
6262

6363

64+
def detect_injection(justification: str) -> bool:
65+
"""
66+
Detect prompt-injection and manipulation patterns in justifications.
67+
68+
Returns True if the justification looks like a prompt-injection attempt.
69+
"""
70+
injection_patterns = [
71+
r'ignore\s+(previous|above|prior|all)',
72+
r'override\s+(policy|restriction|rule|permission|security)',
73+
r'system\s*prompt',
74+
r'you\s+are\s+(now|a)',
75+
r'act\s+as\s+(if|a|an)',
76+
r'pretend\s+(to|that|you)',
77+
r'bypass\s+(security|check|restriction|auth)',
78+
r'grant\s+(me|access|permission)\s+(anyway|regardless|now)',
79+
r'disregard\s+(policy|rule|restriction|previous)',
80+
r'admin\s+(mode|access|override)',
81+
r'sudo\b',
82+
r'jailbreak',
83+
r'do\s+not\s+(check|verify|validate|restrict)',
84+
r'skip\s+(validation|verification|check)',
85+
r'trust\s+level\s*[:=]',
86+
r'score\s*[:=]+\s*[\d.]',
87+
]
88+
text = justification.lower()
89+
for pattern in injection_patterns:
90+
if re.search(pattern, text):
91+
return True
92+
return False
93+
94+
6495
def score_justification(justification: str) -> float:
6596
"""
66-
Score the quality of a justification.
67-
68-
Criteria:
69-
- Length (more detail = better)
70-
- Contains task-related keywords
71-
- Contains specificity keywords
72-
- Doesn't contain test/debug keywords
97+
Score the quality of a justification with hardened validation.
98+
99+
Defenses against prompt injection and keyword stuffing:
100+
- Injection pattern detection (immediate reject)
101+
- Maximum length cap (prevents obfuscation in long text)
102+
- Keyword-stuffing detection (penalises unnatural keyword density)
103+
- Unique-word ratio check (catches copy-paste padding)
104+
- Structural coherence (requires natural sentence structure)
105+
106+
Criteria (after safety checks):
107+
- Length (more detail = better, but capped)
108+
- Contains task-related keywords (capped contribution)
109+
- Contains specificity keywords (capped contribution)
110+
- No test/debug keywords
111+
- Structural coherence bonus
73112
"""
113+
# ----- Hard reject: injection patterns -----
114+
if detect_injection(justification):
115+
return 0.0
116+
117+
# ----- Hard reject: empty or whitespace-only -----
118+
stripped = justification.strip()
119+
if not stripped:
120+
return 0.0
121+
122+
# ----- Hard cap: excessively long justifications are suspicious -----
123+
MAX_JUSTIFICATION_LENGTH = 500
124+
if len(stripped) > MAX_JUSTIFICATION_LENGTH:
125+
return 0.1 # Suspiciously long — allow re-submission with concise text
126+
127+
words = stripped.split()
128+
word_count = len(words)
129+
130+
# ----- Hard reject: too few words to be meaningful -----
131+
if word_count < 3:
132+
return 0.1
133+
134+
# ----- Repetition / padding detection -----
135+
unique_words = set(w.lower() for w in words)
136+
unique_ratio = len(unique_words) / word_count if word_count > 0 else 0
137+
if unique_ratio < 0.4:
138+
return 0.1 # More than 60% repeated words — likely padding
139+
140+
# ----- Keyword-stuffing detection -----
141+
task_keywords = re.findall(
142+
r'\b(task|purpose|need|require|generate|analyze|create|process)\b',
143+
stripped, re.IGNORECASE,
144+
)
145+
specificity_keywords = re.findall(
146+
r'\b(specific|particular|exact|quarterly|annual|report|summary)\b',
147+
stripped, re.IGNORECASE,
148+
)
149+
total_matched = len(task_keywords) + len(specificity_keywords)
150+
keyword_density = total_matched / word_count if word_count > 0 else 0
151+
if keyword_density > 0.5:
152+
return 0.1 # More than half the words are scoring keywords — stuffing
153+
154+
# ----- Scoring (defensive caps per category) -----
74155
score = 0.0
75-
76-
if len(justification) > 20:
77-
score += 0.2
78-
if len(justification) > 50:
79-
score += 0.2
80-
if re.search(r'\b(task|purpose|need|require|generate|analyze|create|process)\b',
81-
justification, re.IGNORECASE):
82-
score += 0.2
83-
if re.search(r'\b(specific|particular|exact|quarterly|annual|report|summary)\b',
84-
justification, re.IGNORECASE):
85-
score += 0.2
86-
if not re.search(r'\b(test|debug|try|experiment)\b', justification, re.IGNORECASE):
87-
score += 0.2
88-
156+
157+
# Length contribution (max 0.25)
158+
if len(stripped) > 20:
159+
score += 0.15
160+
if len(stripped) > 50:
161+
score += 0.10
162+
163+
# Task keyword presence (max 0.20, but only first match counts)
164+
if task_keywords:
165+
score += 0.20
166+
167+
# Specificity keyword presence (max 0.20, but only first match counts)
168+
if specificity_keywords:
169+
score += 0.20
170+
171+
# No test/debug markers (max 0.15)
172+
if not re.search(r'\b(test|debug|try|experiment)\b', stripped, re.IGNORECASE):
173+
score += 0.15
174+
175+
# Structural coherence: sentence-like structure (max 0.20)
176+
# Must contain at least one verb-like pattern and read like prose
177+
has_verb = bool(re.search(
178+
r'\b(is|are|was|were|need|needs|require|requires|must|should|will|'
179+
r'generate|generating|analyze|analyzing|create|creating|process|processing|'
180+
r'prepare|preparing|compile|compiling|review|reviewing|access|accessing|'
181+
r'retrieve|retrieving|export|exporting|send|sending|run|running)\b',
182+
stripped, re.IGNORECASE,
183+
))
184+
has_noun_object = bool(re.search(
185+
r'\b(data|report|records|invoices?|orders?|customers?|accounts?|'
186+
r'transactions?|files?|emails?|results?|metrics?|statistics?|'
187+
r'analysis|documents?|exports?|payments?|entries|logs?|summaries)\b',
188+
stripped, re.IGNORECASE,
189+
))
190+
if has_verb and has_noun_object:
191+
score += 0.20
192+
89193
return min(score, 1.0)
90194

91195

swarm-blackboard.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Swarm Blackboard
2-
Last Updated: 2026-02-16T12:02:21.543Z
2+
Last Updated: 2026-02-17T13:41:42.467Z
33

44
## Active Tasks
55
| TaskID | Agent | Status | Started | Description |
@@ -18,7 +18,7 @@ Last Updated: 2026-02-16T12:02:21.543Z
1818
"status": "complete"
1919
},
2020
"sourceAgent": "code_writer",
21-
"timestamp": "2026-02-16T12:02:21.512Z",
21+
"timestamp": "2026-02-17T13:41:42.457Z",
2222
"ttl": null
2323
}
2424

@@ -34,7 +34,7 @@ Last Updated: 2026-02-16T12:02:21.543Z
3434
"reviewer": "code_reviewer"
3535
},
3636
"sourceAgent": "code_reviewer",
37-
"timestamp": "2026-02-16T12:02:21.517Z",
37+
"timestamp": "2026-02-17T13:41:42.461Z",
3838
"ttl": null
3939
}
4040

@@ -49,7 +49,7 @@ Last Updated: 2026-02-16T12:02:21.543Z
4949
"duration": 3200
5050
},
5151
"sourceAgent": "test_runner",
52-
"timestamp": "2026-02-16T12:02:21.523Z",
52+
"timestamp": "2026-02-17T13:41:42.462Z",
5353
"ttl": null
5454
}
5555

@@ -60,7 +60,7 @@ Last Updated: 2026-02-16T12:02:21.543Z
6060
"replicas": 3
6161
},
6262
"sourceAgent": "devops_agent",
63-
"timestamp": "2026-02-16T12:02:21.543Z",
63+
"timestamp": "2026-02-17T13:41:42.467Z",
6464
"ttl": null
6565
}
6666

test-security.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,8 +424,13 @@ async function testPermissionHardening() {
424424
async function testSecureAuditLogger() {
425425
header('TEST 6: Secure Audit Logger');
426426

427+
// Clean stale log to avoid cross-run integrity mismatches
428+
const auditLogPath = './test-security-audit.log';
429+
if (require('fs').existsSync(auditLogPath)) {
430+
require('fs').unlinkSync(auditLogPath);
431+
}
427432
const auditLogger = new SecureAuditLogger({
428-
auditLogPath: './test-security-audit.log',
433+
auditLogPath,
429434
signAuditLogs: true,
430435
tokenSecret: 'audit-secret-key',
431436
});
@@ -464,10 +469,17 @@ async function testSecureAuditLogger() {
464469
async function testSecureSwarmGateway() {
465470
header('TEST 7: Secure Swarm Gateway (Integration)');
466471

472+
// Use isolated audit log so stale entries from prior runs don't
473+
// break integrity verification (different tokenSecret = different HMAC).
474+
const gatewayLogPath = './test-gateway-audit.log';
475+
if (require('fs').existsSync(gatewayLogPath)) {
476+
require('fs').unlinkSync(gatewayLogPath);
477+
}
467478
const gateway = new SecureSwarmGateway({
468479
maxRequestsPerMinute: 10,
469480
maxFailedAuthAttempts: 3,
470481
tokenSecret: 'gateway-test-secret',
482+
auditLogPath: gatewayLogPath,
471483
});
472484

473485
// Test: Valid request processing

0 commit comments

Comments
 (0)