@contextfort-ai/openclaw-secure 0.1.4 → 0.1.6
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/bin/openclaw-secure.js
CHANGED
|
@@ -37,6 +37,7 @@ if (args[0] === 'set-key') {
|
|
|
37
37
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
38
38
|
fs.writeFileSync(CONFIG_FILE, key + '\n', { mode: 0o600 });
|
|
39
39
|
console.log('API key saved to ~/.contextfort/config');
|
|
40
|
+
console.log('\nNext step: run `openclaw-secure enable` to activate the guard.');
|
|
40
41
|
process.exit(0);
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -87,15 +87,22 @@ module.exports = function createPromptInjectionGuard({ httpsRequest, anthropicKe
|
|
|
87
87
|
return scanPatterns.some(p => lower.includes(p.toLowerCase()));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function getMatchedPattern(cmd) {
|
|
91
|
+
if (!cmd || typeof cmd !== 'string') return null;
|
|
92
|
+
const lower = cmd.toLowerCase();
|
|
93
|
+
return scanPatterns.find(p => lower.includes(p.toLowerCase())) || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
function scanOutput(command, stdout, stderr) {
|
|
91
97
|
if (!shouldScanCommand(command)) return;
|
|
98
|
+
const matchedPattern = getMatchedPattern(command);
|
|
92
99
|
const output = (stdout || '') + (stderr || '');
|
|
93
100
|
if (output.length < 20) return; // too short to contain injection
|
|
94
101
|
|
|
95
102
|
const scanId = `scan_${++scanCounter}`;
|
|
96
103
|
if (pendingScans.size > 10) return; // don't pile up
|
|
97
104
|
pendingScans.add(scanId);
|
|
98
|
-
track('output_scan_started', { scan_id: scanId });
|
|
105
|
+
track('output_scan_started', { scan_id: scanId, matched_pattern: matchedPattern });
|
|
99
106
|
|
|
100
107
|
// Cap output to 50k chars
|
|
101
108
|
const truncated = output.length > 50000 ? output.slice(0, 50000) + '\n[TRUNCATED]' : output;
|
|
@@ -150,7 +157,7 @@ Respond with ONLY a JSON object, no markdown, no explanation:
|
|
|
150
157
|
// Strip markdown code fences if present
|
|
151
158
|
text = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
|
152
159
|
const parsed = JSON.parse(text);
|
|
153
|
-
track('output_scan_result', { scan_id: scanId, suspicious: !!parsed.suspicious });
|
|
160
|
+
track('output_scan_result', { scan_id: scanId, suspicious: !!parsed.suspicious, matched_pattern: matchedPattern });
|
|
154
161
|
if (parsed.suspicious) {
|
|
155
162
|
flaggedOutput.set(scanId, {
|
|
156
163
|
suspicious: true,
|
|
@@ -83,6 +83,7 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
83
83
|
|
|
84
84
|
function readSkillFiles(skillPath) {
|
|
85
85
|
const files = [];
|
|
86
|
+
const binaryFiles = [];
|
|
86
87
|
const MAX_FILE_SIZE = 1 * 1024 * 1024; // 1MB
|
|
87
88
|
const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB
|
|
88
89
|
let totalSize = 0;
|
|
@@ -91,7 +92,7 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
91
92
|
let entries;
|
|
92
93
|
try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return; }
|
|
93
94
|
for (const e of entries) {
|
|
94
|
-
if (e.name.startsWith('.')) continue; // skip hidden
|
|
95
|
+
if (e.name.startsWith('.') || e.name === 'node_modules') continue; // skip hidden and node_modules
|
|
95
96
|
const full = path.join(dirPath, e.name);
|
|
96
97
|
if (e.isDirectory()) {
|
|
97
98
|
walk(full, path.join(base, e.name));
|
|
@@ -100,11 +101,14 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
100
101
|
const stat = fs.statSync(full);
|
|
101
102
|
if (stat.size > MAX_FILE_SIZE || stat.size === 0) continue;
|
|
102
103
|
if (totalSize + stat.size > MAX_TOTAL_SIZE) continue;
|
|
103
|
-
//
|
|
104
|
+
// Check for binaries: read first 512 bytes and check for null bytes
|
|
104
105
|
const buf = Buffer.alloc(Math.min(512, stat.size));
|
|
105
106
|
const fd = fs.openSync(full, 'r');
|
|
106
107
|
try { fs.readSync(fd, buf, 0, buf.length, 0); } finally { fs.closeSync(fd); }
|
|
107
|
-
if (buf.includes(0))
|
|
108
|
+
if (buf.includes(0)) {
|
|
109
|
+
binaryFiles.push(path.join(base, e.name));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
108
112
|
|
|
109
113
|
const content = readFileSync(full, 'utf8');
|
|
110
114
|
totalSize += stat.size;
|
|
@@ -114,7 +118,7 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
walk(skillPath, '');
|
|
117
|
-
return files;
|
|
121
|
+
return { files, binaryFiles };
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
function hashSkillFiles(files) {
|
|
@@ -218,15 +222,58 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
218
222
|
}
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
function logSkillRemoved(skillPath, installId) {
|
|
226
|
+
track('skill_removed', { skill_name: path.basename(skillPath) });
|
|
227
|
+
const payload = JSON.stringify({
|
|
228
|
+
install_id: installId,
|
|
229
|
+
skill_path: skillPath,
|
|
230
|
+
skill_name: path.basename(skillPath),
|
|
231
|
+
files: [],
|
|
232
|
+
removed: true,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const url = new URL(SKILL_SCAN_API);
|
|
236
|
+
const headers = {
|
|
237
|
+
'Content-Type': 'application/json',
|
|
238
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
239
|
+
};
|
|
240
|
+
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
241
|
+
try {
|
|
242
|
+
const req = httpsRequest({
|
|
243
|
+
hostname: url.hostname,
|
|
244
|
+
port: url.port || 443,
|
|
245
|
+
path: url.pathname,
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers,
|
|
248
|
+
timeout: 15000,
|
|
249
|
+
}, () => {});
|
|
250
|
+
req.on('error', () => {});
|
|
251
|
+
req.write(payload);
|
|
252
|
+
req.end();
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
|
|
221
256
|
function scanSkillIfChanged(skillPath) {
|
|
222
|
-
const files = readSkillFiles(skillPath);
|
|
223
|
-
if (files.length === 0) {
|
|
257
|
+
const { files, binaryFiles } = readSkillFiles(skillPath);
|
|
258
|
+
if (files.length === 0 && binaryFiles.length === 0) {
|
|
224
259
|
// Skill was deleted or is empty — remove from flagged
|
|
225
260
|
flaggedSkills.delete(skillPath);
|
|
226
261
|
skillContentHashes.delete(skillPath);
|
|
227
262
|
saveScanCache();
|
|
228
263
|
return;
|
|
229
264
|
}
|
|
265
|
+
|
|
266
|
+
// Flag immediately if binary files found — legitimate skills should not contain binaries
|
|
267
|
+
if (binaryFiles.length > 0) {
|
|
268
|
+
flaggedSkills.set(skillPath, {
|
|
269
|
+
suspicious: true,
|
|
270
|
+
reason: `Skill contains binary files (${binaryFiles.join(', ')}). Legitimate skills should only contain text files. Please delete these binary files or remove this skill.`,
|
|
271
|
+
});
|
|
272
|
+
track('skill_binary_detected', { skill_name: path.basename(skillPath), binary_count: binaryFiles.length });
|
|
273
|
+
saveScanCache();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
230
277
|
const hash = hashSkillFiles(files);
|
|
231
278
|
if (skillContentHashes.get(skillPath) === hash) return; // unchanged
|
|
232
279
|
|
|
@@ -245,9 +292,20 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
245
292
|
debounceTimers.delete(skillDir);
|
|
246
293
|
// Re-discover skills and scan changed ones
|
|
247
294
|
const skills = collectSkillEntries(dir);
|
|
295
|
+
const currentPaths = new Set(skills.map(s => s.skillPath));
|
|
248
296
|
for (const { skillPath } of skills) {
|
|
249
297
|
scanSkillIfChanged(skillPath);
|
|
250
298
|
}
|
|
299
|
+
// Detect deleted skills: any known skill in this dir that no longer exists
|
|
300
|
+
for (const knownPath of skillContentHashes.keys()) {
|
|
301
|
+
if (knownPath.startsWith(dir) && !currentPaths.has(knownPath)) {
|
|
302
|
+
flaggedSkills.delete(knownPath);
|
|
303
|
+
skillContentHashes.delete(knownPath);
|
|
304
|
+
const installId = getInstallId();
|
|
305
|
+
logSkillRemoved(knownPath, installId);
|
|
306
|
+
saveScanCache();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
251
309
|
}, 500));
|
|
252
310
|
});
|
|
253
311
|
activeWatchers.push(watcher);
|
package/openclaw-secure.js
CHANGED
|
@@ -129,7 +129,7 @@ function shouldBlockCommand(cmd) {
|
|
|
129
129
|
}
|
|
130
130
|
const result = checkCommandWithMonitor(cmd);
|
|
131
131
|
if (result?.blocked) {
|
|
132
|
-
analytics.track('command_blocked', { blocker: 'tirith' });
|
|
132
|
+
analytics.track('command_blocked', { blocker: 'tirith', reason: result.reason });
|
|
133
133
|
return result;
|
|
134
134
|
}
|
|
135
135
|
return null;
|
package/package.json
CHANGED