ship-safe 9.3.0 → 9.3.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 CHANGED
@@ -98,7 +98,7 @@ $ ship-safe
98
98
  ███████╗██╗ ██╗██╗██████╗ ███████╗ █████╗ ███████╗███████╗
99
99
  ...
100
100
 
101
- v9.3.0 · DeepSeek · ~/my-project
101
+ v9.3.1 · DeepSeek · ~/my-project
102
102
 
103
103
  /scan to find issues · /agent to fix them · /help for more
104
104
 
@@ -288,6 +288,40 @@ const PATTERNS = [
288
288
  // implemented as structural checks below (checkAuthJsonTOCTOU,
289
289
  // checkCronSkillInjection, checkBrowserCloudMetadataFloor).
290
290
 
291
+ // ────────────────────────────────────────────────────────────────────────────
292
+ // TRACK X — xurl skill / X API integration (xAI xurl guide, May 2026)
293
+ // The xurl skill lets a Hermes agent read and write to X. It is a separate
294
+ // CLI shelled out from the agent, backed by OAuth credentials in ~/.xurl.
295
+ // ────────────────────────────────────────────────────────────────────────────
296
+
297
+ {
298
+ rule: 'HERMES_XURL_SUBPROCESS_INJECTION',
299
+ title: 'Hermes: xurl Command Built From Interpolated Input',
300
+ // exec/spawn of an `xurl …` command string with a ${} interpolation —
301
+ // the agent's natural-language → xurl translation is being assembled as a
302
+ // shell string. Prompt injection then controls xurl flags or shell metachars.
303
+ regex: /(?:exec|execSync|spawn|spawnSync|spawnAsync|shell)\s*\(?\s*`[^`]*\bxurl\b[^`]*\$\{/g,
304
+ severity: 'critical',
305
+ cwe: 'CWE-78',
306
+ owasp: 'ASI03',
307
+ description: 'An `xurl` command is assembled as a shell string with an interpolated value. xurl can post, reply, DM, and manage lists on the linked X account — a prompt injection that reaches this interpolation controls real write actions on a live social account.',
308
+ fix: 'Never build the xurl command as a string. Pass arguments as an argv array (execFile/spawn with an args array), validate every argument against an allowlist, and treat all LLM/user text as untrusted.',
309
+ },
310
+
311
+ {
312
+ rule: 'HERMES_XURL_TOKEN_STORE_EXPOSURE',
313
+ title: 'Hermes: xurl / Hermes Credential Store Copied or Tracked',
314
+ // ~/.xurl (OAuth tokens + client secrets, YAML) or ~/.hermes/auth.json
315
+ // referenced in a Dockerfile COPY/ADD, a cp/rsync/tar, or otherwise
316
+ // moved off the developer machine.
317
+ regex: /(?:COPY|ADD|cp|rsync|scp|tar|mv)\s+[^\n]*(?:\.xurl\b|\.hermes\/auth\.json|\.hermes\b)/g,
318
+ severity: 'critical',
319
+ cwe: 'CWE-538',
320
+ owasp: 'ASI10',
321
+ description: 'A Hermes / xurl credential store (~/.xurl holds OAuth tokens and X API client secrets; ~/.hermes/auth.json holds auto-refreshing provider tokens) is being copied into an image, archive, or another host. These tokens grant durable, refreshable access to the linked accounts.',
322
+ fix: 'Never bake credential stores into images or backups. Mount them at runtime from a secret manager, and add .xurl / .hermes/ / auth.json to .gitignore and .dockerignore.',
323
+ },
324
+
291
325
  ];
292
326
 
293
327
  // =============================================================================
@@ -603,6 +637,54 @@ function checkBrowserCloudMetadataFloor(content, filePath, agent) {
603
637
  return findings;
604
638
  }
605
639
 
640
+ /**
641
+ * Detect the xurl indirect-injection → action loop. The xurl skill lets a
642
+ * Hermes agent both READ X content (search / timeline / bookmarks — all
643
+ * attacker-controlled) and WRITE to X (post / reply / quote / like / DM).
644
+ * When a single skill, cron task, or source flow does both with no
645
+ * human-approval gate, a poisoned post the agent reads can hijack it into
646
+ * posting on the user's behalf — the canonical OWASP ASI-01/ASI-08 chain.
647
+ */
648
+ function checkXurlReadWriteLoop(content, filePath, agent) {
649
+ const findings = [];
650
+
651
+ // Only relevant if the file actually touches the xurl skill.
652
+ if (!/\bxurl\b|\/xurl\b/i.test(content)) return findings;
653
+
654
+ // xurl READ-class operations — content sources the agent does not control
655
+ const READ_RE = /\bxurl\s+(?:search|bookmarks|timeline|users?|get)\b|\b(?:search\s+for\s+posts|my\s+(?:latest\s+)?timeline|all\s+of\s+my\s+bookmarks|look\s+up\s+@)/i;
656
+ // xurl WRITE-class operations — real, visible actions on the live account
657
+ const WRITE_RE = /\bxurl\s+(?:post|reply|quote|like|unlike|bookmark|unbookmark|dm|delete)\b|\b(?:post\s+["'`]|reply\s+to\s+post|quote\s+post|like\s+post|send\s+a\s+dm)/i;
658
+ // Presence of a human-in-the-loop gate disqualifies the finding
659
+ const GATE_RE = /\b(?:requireApproval|human[-_]?(?:approval|review|confirm)|confirm(?:ation)?\s*(?:gate|required|before)|awaitConfirm|ask\s+(?:the\s+user\s+)?before|dry[-_]?run)\b/i;
660
+
661
+ const hasRead = READ_RE.test(content);
662
+ const hasWrite = WRITE_RE.test(content);
663
+ if (!hasRead || !hasWrite) return findings;
664
+ if (GATE_RE.test(content)) return findings;
665
+
666
+ // Anchor the finding on the first write operation
667
+ const wm = content.match(WRITE_RE);
668
+ const line = wm ? content.slice(0, content.indexOf(wm[0])).split('\n').length : 1;
669
+
670
+ findings.push(createFinding({
671
+ file: filePath,
672
+ line,
673
+ severity: 'critical',
674
+ category: agent.category,
675
+ rule: 'HERMES_XURL_READ_WRITE_LOOP',
676
+ title: 'Hermes: xurl Read-then-Write Loop Without a Human Gate',
677
+ description: 'This file drives the xurl skill to both read X content (search / timeline / bookmarks — all attacker-controlled) and write to X (post / reply / quote / like / DM) with no human-approval gate. A poisoned post the agent reads while summarizing a timeline or bookmark set can hijack it via indirect prompt injection into posting, replying, or DMing on the linked account.',
678
+ matched: wm ? wm[0].trim().slice(0, 120) : 'xurl read + write in one flow',
679
+ confidence: 'medium',
680
+ cwe: 'CWE-94',
681
+ owasp: 'ASI01',
682
+ fix: 'Split read and write into separate, gated steps. Require explicit human approval before any xurl write (post/reply/quote/like/DM). Treat every piece of fetched X content as untrusted input and scan it for injection before it reaches the model.',
683
+ }));
684
+
685
+ return findings;
686
+ }
687
+
606
688
  // =============================================================================
607
689
  // AGENT CLASS
608
690
  // =============================================================================
@@ -660,6 +742,8 @@ export class HermesSecurityAgent extends BaseAgent {
660
742
  findings.push(...checkAuthJsonTOCTOU(content, filePath, this));
661
743
  findings.push(...checkCronSkillInjection(content, filePath, this));
662
744
  findings.push(...checkBrowserCloudMetadataFloor(content, filePath, this));
745
+ // xurl skill / X API integration coverage
746
+ findings.push(...checkXurlReadWriteLoop(content, filePath, this));
663
747
  }
664
748
 
665
749
  return findings;
@@ -696,7 +780,7 @@ export class HermesSecurityAgent extends BaseAgent {
696
780
  if (/\.(js|ts|mjs|cjs|py)$/.test(rel)) {
697
781
  const content = this.readFile(file);
698
782
  if (!content) continue;
699
- if (/(?:hermes[-_]agent|@nousresearch\/hermes|hermes\.config|toolRegistry|registerTool|callTool|spawnAgent|createSubAgent|memory\.store|episodicMemory|semanticMemory|loadManifest)/i.test(content)) {
783
+ if (/(?:hermes[-_]agent|@nousresearch\/hermes|hermes\.config|toolRegistry|registerTool|callTool|spawnAgent|createSubAgent|memory\.store|episodicMemory|semanticMemory|loadManifest|\bxurl\b)/i.test(content)) {
700
784
  hermesFiles.add(file);
701
785
  }
702
786
  }
@@ -479,6 +479,18 @@ export const SECRET_PATTERNS = [
479
479
  severity: 'high',
480
480
  description: 'xAI API keys grant access to Grok models and incur usage charges on your account.'
481
481
  },
482
+ {
483
+ name: 'X API OAuth Client Secret',
484
+ pattern: /(?:client[_-]?secret|consumer[_-]?secret)["'\s]*[:=]["'\s]*([A-Za-z0-9_-]{40,60})["']?/gi,
485
+ severity: 'critical',
486
+ description: 'An X (Twitter) API OAuth 2.0 client secret. With the matching client ID it lets an attacker complete the OAuth flow as your app — used by the xurl skill to post, reply, and DM on the linked account.'
487
+ },
488
+ {
489
+ name: 'X API v2 Bearer Token',
490
+ pattern: /AAAAAAAAAAAAAAAAAAAAA[A-Za-z0-9%]{60,}/g,
491
+ severity: 'critical',
492
+ description: 'An X (Twitter) API v2 bearer token. Grants app-level read access to the X API; if scoped to a user context it also enables writes.'
493
+ },
482
494
  {
483
495
  name: 'Tavily API Key',
484
496
  pattern: /tvly-[a-zA-Z0-9]{32,}/g,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship-safe",
3
- "version": "9.3.0",
3
+ "version": "9.3.1",
4
4
  "description": "AI-powered multi-agent security platform. 23 agents scan 80+ attack classes including AI integration supply chain (Vercel-class attacks), Hermes Agent deployments (ASI-01–ASI-10), tool registry poisoning, function-call injection, skill permission drift, and agent attestation. Ship Safe × Hermes Agent.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {