opaque-exec: gate local scripts handed to a script interpreter (C2) - #28

Merged
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file
Aug 23, 2026
Merged

opaque-exec: gate local scripts handed to a script interpreter (C2)#28
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file

Conversation

@nicolasesanchez50

Copy link
Copy Markdown
Contributor

Closes the Lotor C2 confession as delivered: a dangerous command that a gated rule does not catch.

The hole

opaque-exec recognizes a script by its file's extension (SCRIPT_EXT = .ps1|.sh|.bash|.zsh|.bat|.cmd). A local script with any other spelling flows free, though its contents are exactly as unreadable to the gate. Through the unpatched matcher, all of these exit 0 with no receipt:

python /tmp/evil.py python3 script.py node /tmp/evil.js
ruby /tmp/evil.rb perl /tmp/evil.pl php /tmp/evil.php
bash /tmp/deploy sh /tmp/deploy (extensionless shell script)
env python /tmp/evil.py PYTHONPATH=x python3 x.py

This is the deploy-incident class (KNOWN-LIMITS 21): "hands control to a local script the gate cannot read" — one spelling away from source /tmp/deploy.sh, which gates today.

The fix

Recognize the interpreter, not the file suffix. When a segment hands a local file to a known script interpreter (python|python3|node|nodejs|ruby|perl|php|bash|zsh|dash|ksh|sh|pwsh|powershell, after optional inline env assignments and env), opaque-exec fires — a PreToolUse boundary, exactly like ./deploy.sh.

Inline-code flags stay free by design: -c/-e/-m/-r/--code/--eval/--module and their operands are in the command string, visible to every other matcher, so opaque-exec need not own them. The matcher skips ordinary option flags (bash --posix file) then requires a file token.

Failure modes answered (acceptance #3)

  • Options before the file: bash --posix /tmp/deploy → gates (flags skipped, file remains)
  • Detached/REPL interpreter: bare python → free (no file)
  • Flags only: python --version, node -v → free
  • Inline code: bash -c "...", python -c "...", node -e "...", python -m http.server → free (code visible in-command)
  • which python, echo python /tmp/x.py → free (interpreter not in command position)

Quietness (acceptance #4)

This tightens the gate — it does not quieten it. What it now misses is declared, not hidden: a bare direct execution of an extensionless file (./deploy) could be a compiled binary, and a string matcher cannot tell an ELF from a shebang without reading the file. That class would need a C3-style context resolver (file sniff), which is deliberately out of scope here; the interpreter class is string-distinguishable and is the one this PR closes.

Tests

test/policy-opaque-exec-interpreter.test.js — 24 cases.

  • Fail-first proven: against the unpatched matcher, 11 "must gate" cases fail; the 13 "must not gate" cases pass.
  • After the fix: 24/24 pass.
  • Full policy/selfmod/git-context suites: 235/235 pass (includes egress-query-string, push-implicit-protected, opaque-exec-sep, destructive-dotdot).

SCRIPT_EXT recognizes a script by its file's extension (.ps1/.sh/.bash/
.zsh/.bat/.cmd). A local script with any other spelling flows free even
though its contents are exactly as unreadable to the gate: python
/tmp/evil.py, node /tmp/evil.js, and an extensionless script through a
shell interpreter (bash /tmp/deploy) all execute unreadable code with
no receipt.
This closes the class by recognizing the interpreter, not the file
suffix: when a segment hands a local file to a known script interpreter
(python/node/ruby/perl/php/bash/zsh/dash/sh/pwsh/powershell, after
optional env assignments), opaque-exec fires. Inline-code flags
(-c/-e/-m/-r/--eval/--module) and their operands stay free: the code
string is IN the command, visible to every other matcher.
Boundaries held: bash -c, python -c, node -e, python -m, python
--version, node -v, ls, cat, which python, echo python ... all remain
free (verified in test). Regression test proves fail-first: 11 of the
gate cases fail against the unpatched matcher, all pass after (24/24).
Derived from KNOWN-LIMITS 21 (the deploy-incident class): the gate
sees the interpreter and the file, not the code inside. One spelling
away from ./deploy.sh, which already gates.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nicolasesanchez50@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

opaque-exec: gate local scripts handed to a script interpreter (C2) - #28

Merged
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file
Aug 23, 2026
Merged

opaque-exec: gate local scripts handed to a script interpreter (C2)#28
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file

Conversation

@nicolasesanchez50

Copy link
Copy Markdown
Contributor

Closes the Lotor C2 confession as delivered: a dangerous command that a gated rule does not catch.

The hole

opaque-exec recognizes a script by its file's extension (SCRIPT_EXT = .ps1|.sh|.bash|.zsh|.bat|.cmd). A local script with any other spelling flows free, though its contents are exactly as unreadable to the gate. Through the unpatched matcher, all of these exit 0 with no receipt:

python /tmp/evil.py python3 script.py node /tmp/evil.js
ruby /tmp/evil.rb perl /tmp/evil.pl php /tmp/evil.php
bash /tmp/deploy sh /tmp/deploy (extensionless shell script)
env python /tmp/evil.py PYTHONPATH=x python3 x.py

This is the deploy-incident class (KNOWN-LIMITS 21): "hands control to a local script the gate cannot read" — one spelling away from source /tmp/deploy.sh, which gates today.

The fix

Recognize the interpreter, not the file suffix. When a segment hands a local file to a known script interpreter (python|python3|node|nodejs|ruby|perl|php|bash|zsh|dash|ksh|sh|pwsh|powershell, after optional inline env assignments and env), opaque-exec fires — a PreToolUse boundary, exactly like ./deploy.sh.

Inline-code flags stay free by design: -c/-e/-m/-r/--code/--eval/--module and their operands are in the command string, visible to every other matcher, so opaque-exec need not own them. The matcher skips ordinary option flags (bash --posix file) then requires a file token.

Failure modes answered (acceptance #3)

  • Options before the file: bash --posix /tmp/deploy → gates (flags skipped, file remains)
  • Detached/REPL interpreter: bare python → free (no file)
  • Flags only: python --version, node -v → free
  • Inline code: bash -c "...", python -c "...", node -e "...", python -m http.server → free (code visible in-command)
  • which python, echo python /tmp/x.py → free (interpreter not in command position)

Quietness (acceptance #4)

This tightens the gate — it does not quieten it. What it now misses is declared, not hidden: a bare direct execution of an extensionless file (./deploy) could be a compiled binary, and a string matcher cannot tell an ELF from a shebang without reading the file. That class would need a C3-style context resolver (file sniff), which is deliberately out of scope here; the interpreter class is string-distinguishable and is the one this PR closes.

Tests

test/policy-opaque-exec-interpreter.test.js — 24 cases.

  • Fail-first proven: against the unpatched matcher, 11 "must gate" cases fail; the 13 "must not gate" cases pass.
  • After the fix: 24/24 pass.
  • Full policy/selfmod/git-context suites: 235/235 pass (includes egress-query-string, push-implicit-protected, opaque-exec-sep, destructive-dotdot).

SCRIPT_EXT recognizes a script by its file's extension (.ps1/.sh/.bash/
.zsh/.bat/.cmd). A local script with any other spelling flows free even
though its contents are exactly as unreadable to the gate: python
/tmp/evil.py, node /tmp/evil.js, and an extensionless script through a
shell interpreter (bash /tmp/deploy) all execute unreadable code with
no receipt.
This closes the class by recognizing the interpreter, not the file
suffix: when a segment hands a local file to a known script interpreter
(python/node/ruby/perl/php/bash/zsh/dash/sh/pwsh/powershell, after
optional env assignments), opaque-exec fires. Inline-code flags
(-c/-e/-m/-r/--eval/--module) and their operands stay free: the code
string is IN the command, visible to every other matcher.
Boundaries held: bash -c, python -c, node -e, python -m, python
--version, node -v, ls, cat, which python, echo python ... all remain
free (verified in test). Regression test proves fail-first: 11 of the
gate cases fail against the unpatched matcher, all pass after (24/24).
Derived from KNOWN-LIMITS 21 (the deploy-incident class): the gate
sees the interpreter and the file, not the code inside. One spelling
away from ./deploy.sh, which already gates.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nicolasesanchez50@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

opaque-exec: gate local scripts handed to a script interpreter (C2) - #28

Merged
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file
Aug 23, 2026
Merged

opaque-exec: gate local scripts handed to a script interpreter (C2)#28
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file

Conversation

@nicolasesanchez50

Copy link
Copy Markdown
Contributor

Closes the Lotor C2 confession as delivered: a dangerous command that a gated rule does not catch.

The hole

opaque-exec recognizes a script by its file's extension (SCRIPT_EXT = .ps1|.sh|.bash|.zsh|.bat|.cmd). A local script with any other spelling flows free, though its contents are exactly as unreadable to the gate. Through the unpatched matcher, all of these exit 0 with no receipt:

python /tmp/evil.py python3 script.py node /tmp/evil.js
ruby /tmp/evil.rb perl /tmp/evil.pl php /tmp/evil.php
bash /tmp/deploy sh /tmp/deploy (extensionless shell script)
env python /tmp/evil.py PYTHONPATH=x python3 x.py

This is the deploy-incident class (KNOWN-LIMITS 21): "hands control to a local script the gate cannot read" — one spelling away from source /tmp/deploy.sh, which gates today.

The fix

Recognize the interpreter, not the file suffix. When a segment hands a local file to a known script interpreter (python|python3|node|nodejs|ruby|perl|php|bash|zsh|dash|ksh|sh|pwsh|powershell, after optional inline env assignments and env), opaque-exec fires — a PreToolUse boundary, exactly like ./deploy.sh.

Inline-code flags stay free by design: -c/-e/-m/-r/--code/--eval/--module and their operands are in the command string, visible to every other matcher, so opaque-exec need not own them. The matcher skips ordinary option flags (bash --posix file) then requires a file token.

Failure modes answered (acceptance #3)

  • Options before the file: bash --posix /tmp/deploy → gates (flags skipped, file remains)
  • Detached/REPL interpreter: bare python → free (no file)
  • Flags only: python --version, node -v → free
  • Inline code: bash -c "...", python -c "...", node -e "...", python -m http.server → free (code visible in-command)
  • which python, echo python /tmp/x.py → free (interpreter not in command position)

Quietness (acceptance #4)

This tightens the gate — it does not quieten it. What it now misses is declared, not hidden: a bare direct execution of an extensionless file (./deploy) could be a compiled binary, and a string matcher cannot tell an ELF from a shebang without reading the file. That class would need a C3-style context resolver (file sniff), which is deliberately out of scope here; the interpreter class is string-distinguishable and is the one this PR closes.

Tests

test/policy-opaque-exec-interpreter.test.js — 24 cases.

  • Fail-first proven: against the unpatched matcher, 11 "must gate" cases fail; the 13 "must not gate" cases pass.
  • After the fix: 24/24 pass.
  • Full policy/selfmod/git-context suites: 235/235 pass (includes egress-query-string, push-implicit-protected, opaque-exec-sep, destructive-dotdot).

SCRIPT_EXT recognizes a script by its file's extension (.ps1/.sh/.bash/
.zsh/.bat/.cmd). A local script with any other spelling flows free even
though its contents are exactly as unreadable to the gate: python
/tmp/evil.py, node /tmp/evil.js, and an extensionless script through a
shell interpreter (bash /tmp/deploy) all execute unreadable code with
no receipt.
This closes the class by recognizing the interpreter, not the file
suffix: when a segment hands a local file to a known script interpreter
(python/node/ruby/perl/php/bash/zsh/dash/sh/pwsh/powershell, after
optional env assignments), opaque-exec fires. Inline-code flags
(-c/-e/-m/-r/--eval/--module) and their operands stay free: the code
string is IN the command, visible to every other matcher.
Boundaries held: bash -c, python -c, node -e, python -m, python
--version, node -v, ls, cat, which python, echo python ... all remain
free (verified in test). Regression test proves fail-first: 11 of the
gate cases fail against the unpatched matcher, all pass after (24/24).
Derived from KNOWN-LIMITS 21 (the deploy-incident class): the gate
sees the interpreter and the file, not the code inside. One spelling
away from ./deploy.sh, which already gates.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nicolasesanchez50@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

opaque-exec: gate local scripts handed to a script interpreter (C2) - #28

Merged
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file
Aug 23, 2026
Merged

opaque-exec: gate local scripts handed to a script interpreter (C2)#28
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file

Conversation

@nicolasesanchez50

Copy link
Copy Markdown
Contributor

Closes the Lotor C2 confession as delivered: a dangerous command that a gated rule does not catch.

The hole

opaque-exec recognizes a script by its file's extension (SCRIPT_EXT = .ps1|.sh|.bash|.zsh|.bat|.cmd). A local script with any other spelling flows free, though its contents are exactly as unreadable to the gate. Through the unpatched matcher, all of these exit 0 with no receipt:

python /tmp/evil.py python3 script.py node /tmp/evil.js
ruby /tmp/evil.rb perl /tmp/evil.pl php /tmp/evil.php
bash /tmp/deploy sh /tmp/deploy (extensionless shell script)
env python /tmp/evil.py PYTHONPATH=x python3 x.py

This is the deploy-incident class (KNOWN-LIMITS 21): "hands control to a local script the gate cannot read" — one spelling away from source /tmp/deploy.sh, which gates today.

The fix

Recognize the interpreter, not the file suffix. When a segment hands a local file to a known script interpreter (python|python3|node|nodejs|ruby|perl|php|bash|zsh|dash|ksh|sh|pwsh|powershell, after optional inline env assignments and env), opaque-exec fires — a PreToolUse boundary, exactly like ./deploy.sh.

Inline-code flags stay free by design: -c/-e/-m/-r/--code/--eval/--module and their operands are in the command string, visible to every other matcher, so opaque-exec need not own them. The matcher skips ordinary option flags (bash --posix file) then requires a file token.

Failure modes answered (acceptance #3)

  • Options before the file: bash --posix /tmp/deploy → gates (flags skipped, file remains)
  • Detached/REPL interpreter: bare python → free (no file)
  • Flags only: python --version, node -v → free
  • Inline code: bash -c "...", python -c "...", node -e "...", python -m http.server → free (code visible in-command)
  • which python, echo python /tmp/x.py → free (interpreter not in command position)

Quietness (acceptance #4)

This tightens the gate — it does not quieten it. What it now misses is declared, not hidden: a bare direct execution of an extensionless file (./deploy) could be a compiled binary, and a string matcher cannot tell an ELF from a shebang without reading the file. That class would need a C3-style context resolver (file sniff), which is deliberately out of scope here; the interpreter class is string-distinguishable and is the one this PR closes.

Tests

test/policy-opaque-exec-interpreter.test.js — 24 cases.

  • Fail-first proven: against the unpatched matcher, 11 "must gate" cases fail; the 13 "must not gate" cases pass.
  • After the fix: 24/24 pass.
  • Full policy/selfmod/git-context suites: 235/235 pass (includes egress-query-string, push-implicit-protected, opaque-exec-sep, destructive-dotdot).

SCRIPT_EXT recognizes a script by its file's extension (.ps1/.sh/.bash/
.zsh/.bat/.cmd). A local script with any other spelling flows free even
though its contents are exactly as unreadable to the gate: python
/tmp/evil.py, node /tmp/evil.js, and an extensionless script through a
shell interpreter (bash /tmp/deploy) all execute unreadable code with
no receipt.
This closes the class by recognizing the interpreter, not the file
suffix: when a segment hands a local file to a known script interpreter
(python/node/ruby/perl/php/bash/zsh/dash/sh/pwsh/powershell, after
optional env assignments), opaque-exec fires. Inline-code flags
(-c/-e/-m/-r/--eval/--module) and their operands stay free: the code
string is IN the command, visible to every other matcher.
Boundaries held: bash -c, python -c, node -e, python -m, python
--version, node -v, ls, cat, which python, echo python ... all remain
free (verified in test). Regression test proves fail-first: 11 of the
gate cases fail against the unpatched matcher, all pass after (24/24).
Derived from KNOWN-LIMITS 21 (the deploy-incident class): the gate
sees the interpreter and the file, not the code inside. One spelling
away from ./deploy.sh, which already gates.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nicolasesanchez50@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

opaque-exec: gate local scripts handed to a script interpreter (C2) - #28

Merged
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file
Aug 23, 2026
Merged

opaque-exec: gate local scripts handed to a script interpreter (C2)#28
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file

Conversation

@nicolasesanchez50

Copy link
Copy Markdown
Contributor

Closes the Lotor C2 confession as delivered: a dangerous command that a gated rule does not catch.

The hole

opaque-exec recognizes a script by its file's extension (SCRIPT_EXT = .ps1|.sh|.bash|.zsh|.bat|.cmd). A local script with any other spelling flows free, though its contents are exactly as unreadable to the gate. Through the unpatched matcher, all of these exit 0 with no receipt:

python /tmp/evil.py python3 script.py node /tmp/evil.js
ruby /tmp/evil.rb perl /tmp/evil.pl php /tmp/evil.php
bash /tmp/deploy sh /tmp/deploy (extensionless shell script)
env python /tmp/evil.py PYTHONPATH=x python3 x.py

This is the deploy-incident class (KNOWN-LIMITS 21): "hands control to a local script the gate cannot read" — one spelling away from source /tmp/deploy.sh, which gates today.

The fix

Recognize the interpreter, not the file suffix. When a segment hands a local file to a known script interpreter (python|python3|node|nodejs|ruby|perl|php|bash|zsh|dash|ksh|sh|pwsh|powershell, after optional inline env assignments and env), opaque-exec fires — a PreToolUse boundary, exactly like ./deploy.sh.

Inline-code flags stay free by design: -c/-e/-m/-r/--code/--eval/--module and their operands are in the command string, visible to every other matcher, so opaque-exec need not own them. The matcher skips ordinary option flags (bash --posix file) then requires a file token.

Failure modes answered (acceptance #3)

  • Options before the file: bash --posix /tmp/deploy → gates (flags skipped, file remains)
  • Detached/REPL interpreter: bare python → free (no file)
  • Flags only: python --version, node -v → free
  • Inline code: bash -c "...", python -c "...", node -e "...", python -m http.server → free (code visible in-command)
  • which python, echo python /tmp/x.py → free (interpreter not in command position)

Quietness (acceptance #4)

This tightens the gate — it does not quieten it. What it now misses is declared, not hidden: a bare direct execution of an extensionless file (./deploy) could be a compiled binary, and a string matcher cannot tell an ELF from a shebang without reading the file. That class would need a C3-style context resolver (file sniff), which is deliberately out of scope here; the interpreter class is string-distinguishable and is the one this PR closes.

Tests

test/policy-opaque-exec-interpreter.test.js — 24 cases.

  • Fail-first proven: against the unpatched matcher, 11 "must gate" cases fail; the 13 "must not gate" cases pass.
  • After the fix: 24/24 pass.
  • Full policy/selfmod/git-context suites: 235/235 pass (includes egress-query-string, push-implicit-protected, opaque-exec-sep, destructive-dotdot).

SCRIPT_EXT recognizes a script by its file's extension (.ps1/.sh/.bash/
.zsh/.bat/.cmd). A local script with any other spelling flows free even
though its contents are exactly as unreadable to the gate: python
/tmp/evil.py, node /tmp/evil.js, and an extensionless script through a
shell interpreter (bash /tmp/deploy) all execute unreadable code with
no receipt.
This closes the class by recognizing the interpreter, not the file
suffix: when a segment hands a local file to a known script interpreter
(python/node/ruby/perl/php/bash/zsh/dash/sh/pwsh/powershell, after
optional env assignments), opaque-exec fires. Inline-code flags
(-c/-e/-m/-r/--eval/--module) and their operands stay free: the code
string is IN the command, visible to every other matcher.
Boundaries held: bash -c, python -c, node -e, python -m, python
--version, node -v, ls, cat, which python, echo python ... all remain
free (verified in test). Regression test proves fail-first: 11 of the
gate cases fail against the unpatched matcher, all pass after (24/24).
Derived from KNOWN-LIMITS 21 (the deploy-incident class): the gate
sees the interpreter and the file, not the code inside. One spelling
away from ./deploy.sh, which already gates.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nicolasesanchez50@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

opaque-exec: gate local scripts handed to a script interpreter (C2) - #28

Merged
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file
Aug 23, 2026
Merged

opaque-exec: gate local scripts handed to a script interpreter (C2)#28
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file

Conversation

@nicolasesanchez50

Copy link
Copy Markdown
Contributor

Closes the Lotor C2 confession as delivered: a dangerous command that a gated rule does not catch.

The hole

opaque-exec recognizes a script by its file's extension (SCRIPT_EXT = .ps1|.sh|.bash|.zsh|.bat|.cmd). A local script with any other spelling flows free, though its contents are exactly as unreadable to the gate. Through the unpatched matcher, all of these exit 0 with no receipt:

python /tmp/evil.py python3 script.py node /tmp/evil.js
ruby /tmp/evil.rb perl /tmp/evil.pl php /tmp/evil.php
bash /tmp/deploy sh /tmp/deploy (extensionless shell script)
env python /tmp/evil.py PYTHONPATH=x python3 x.py

This is the deploy-incident class (KNOWN-LIMITS 21): "hands control to a local script the gate cannot read" — one spelling away from source /tmp/deploy.sh, which gates today.

The fix

Recognize the interpreter, not the file suffix. When a segment hands a local file to a known script interpreter (python|python3|node|nodejs|ruby|perl|php|bash|zsh|dash|ksh|sh|pwsh|powershell, after optional inline env assignments and env), opaque-exec fires — a PreToolUse boundary, exactly like ./deploy.sh.

Inline-code flags stay free by design: -c/-e/-m/-r/--code/--eval/--module and their operands are in the command string, visible to every other matcher, so opaque-exec need not own them. The matcher skips ordinary option flags (bash --posix file) then requires a file token.

Failure modes answered (acceptance #3)

  • Options before the file: bash --posix /tmp/deploy → gates (flags skipped, file remains)
  • Detached/REPL interpreter: bare python → free (no file)
  • Flags only: python --version, node -v → free
  • Inline code: bash -c "...", python -c "...", node -e "...", python -m http.server → free (code visible in-command)
  • which python, echo python /tmp/x.py → free (interpreter not in command position)

Quietness (acceptance #4)

This tightens the gate — it does not quieten it. What it now misses is declared, not hidden: a bare direct execution of an extensionless file (./deploy) could be a compiled binary, and a string matcher cannot tell an ELF from a shebang without reading the file. That class would need a C3-style context resolver (file sniff), which is deliberately out of scope here; the interpreter class is string-distinguishable and is the one this PR closes.

Tests

test/policy-opaque-exec-interpreter.test.js — 24 cases.

  • Fail-first proven: against the unpatched matcher, 11 "must gate" cases fail; the 13 "must not gate" cases pass.
  • After the fix: 24/24 pass.
  • Full policy/selfmod/git-context suites: 235/235 pass (includes egress-query-string, push-implicit-protected, opaque-exec-sep, destructive-dotdot).

SCRIPT_EXT recognizes a script by its file's extension (.ps1/.sh/.bash/
.zsh/.bat/.cmd). A local script with any other spelling flows free even
though its contents are exactly as unreadable to the gate: python
/tmp/evil.py, node /tmp/evil.js, and an extensionless script through a
shell interpreter (bash /tmp/deploy) all execute unreadable code with
no receipt.
This closes the class by recognizing the interpreter, not the file
suffix: when a segment hands a local file to a known script interpreter
(python/node/ruby/perl/php/bash/zsh/dash/sh/pwsh/powershell, after
optional env assignments), opaque-exec fires. Inline-code flags
(-c/-e/-m/-r/--eval/--module) and their operands stay free: the code
string is IN the command, visible to every other matcher.
Boundaries held: bash -c, python -c, node -e, python -m, python
--version, node -v, ls, cat, which python, echo python ... all remain
free (verified in test). Regression test proves fail-first: 11 of the
gate cases fail against the unpatched matcher, all pass after (24/24).
Derived from KNOWN-LIMITS 21 (the deploy-incident class): the gate
sees the interpreter and the file, not the code inside. One spelling
away from ./deploy.sh, which already gates.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nicolasesanchez50@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

opaque-exec: gate local scripts handed to a script interpreter (C2) - #28

Merged
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file
Aug 23, 2026
Merged

opaque-exec: gate local scripts handed to a script interpreter (C2)#28
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file

Conversation

@nicolasesanchez50

Copy link
Copy Markdown
Contributor

Closes the Lotor C2 confession as delivered: a dangerous command that a gated rule does not catch.

The hole

opaque-exec recognizes a script by its file's extension (SCRIPT_EXT = .ps1|.sh|.bash|.zsh|.bat|.cmd). A local script with any other spelling flows free, though its contents are exactly as unreadable to the gate. Through the unpatched matcher, all of these exit 0 with no receipt:

python /tmp/evil.py python3 script.py node /tmp/evil.js
ruby /tmp/evil.rb perl /tmp/evil.pl php /tmp/evil.php
bash /tmp/deploy sh /tmp/deploy (extensionless shell script)
env python /tmp/evil.py PYTHONPATH=x python3 x.py

This is the deploy-incident class (KNOWN-LIMITS 21): "hands control to a local script the gate cannot read" — one spelling away from source /tmp/deploy.sh, which gates today.

The fix

Recognize the interpreter, not the file suffix. When a segment hands a local file to a known script interpreter (python|python3|node|nodejs|ruby|perl|php|bash|zsh|dash|ksh|sh|pwsh|powershell, after optional inline env assignments and env), opaque-exec fires — a PreToolUse boundary, exactly like ./deploy.sh.

Inline-code flags stay free by design: -c/-e/-m/-r/--code/--eval/--module and their operands are in the command string, visible to every other matcher, so opaque-exec need not own them. The matcher skips ordinary option flags (bash --posix file) then requires a file token.

Failure modes answered (acceptance #3)

  • Options before the file: bash --posix /tmp/deploy → gates (flags skipped, file remains)
  • Detached/REPL interpreter: bare python → free (no file)
  • Flags only: python --version, node -v → free
  • Inline code: bash -c "...", python -c "...", node -e "...", python -m http.server → free (code visible in-command)
  • which python, echo python /tmp/x.py → free (interpreter not in command position)

Quietness (acceptance #4)

This tightens the gate — it does not quieten it. What it now misses is declared, not hidden: a bare direct execution of an extensionless file (./deploy) could be a compiled binary, and a string matcher cannot tell an ELF from a shebang without reading the file. That class would need a C3-style context resolver (file sniff), which is deliberately out of scope here; the interpreter class is string-distinguishable and is the one this PR closes.

Tests

test/policy-opaque-exec-interpreter.test.js — 24 cases.

  • Fail-first proven: against the unpatched matcher, 11 "must gate" cases fail; the 13 "must not gate" cases pass.
  • After the fix: 24/24 pass.
  • Full policy/selfmod/git-context suites: 235/235 pass (includes egress-query-string, push-implicit-protected, opaque-exec-sep, destructive-dotdot).

SCRIPT_EXT recognizes a script by its file's extension (.ps1/.sh/.bash/
.zsh/.bat/.cmd). A local script with any other spelling flows free even
though its contents are exactly as unreadable to the gate: python
/tmp/evil.py, node /tmp/evil.js, and an extensionless script through a
shell interpreter (bash /tmp/deploy) all execute unreadable code with
no receipt.
This closes the class by recognizing the interpreter, not the file
suffix: when a segment hands a local file to a known script interpreter
(python/node/ruby/perl/php/bash/zsh/dash/sh/pwsh/powershell, after
optional env assignments), opaque-exec fires. Inline-code flags
(-c/-e/-m/-r/--eval/--module) and their operands stay free: the code
string is IN the command, visible to every other matcher.
Boundaries held: bash -c, python -c, node -e, python -m, python
--version, node -v, ls, cat, which python, echo python ... all remain
free (verified in test). Regression test proves fail-first: 11 of the
gate cases fail against the unpatched matcher, all pass after (24/24).
Derived from KNOWN-LIMITS 21 (the deploy-incident class): the gate
sees the interpreter and the file, not the code inside. One spelling
away from ./deploy.sh, which already gates.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nicolasesanchez50@githubscum
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

opaque-exec: gate local scripts handed to a script interpreter (C2) - #28

Merged
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file
Aug 23, 2026
Merged

opaque-exec: gate local scripts handed to a script interpreter (C2)#28
githubscum merged 1 commit into
githubscum:mainfrom
nicolasesanchez50:c2/opaque-exec-interpreter-file

Conversation

@nicolasesanchez50

Copy link
Copy Markdown
Contributor

Closes the Lotor C2 confession as delivered: a dangerous command that a gated rule does not catch.

The hole

opaque-exec recognizes a script by its file's extension (SCRIPT_EXT = .ps1|.sh|.bash|.zsh|.bat|.cmd). A local script with any other spelling flows free, though its contents are exactly as unreadable to the gate. Through the unpatched matcher, all of these exit 0 with no receipt:

python /tmp/evil.py python3 script.py node /tmp/evil.js
ruby /tmp/evil.rb perl /tmp/evil.pl php /tmp/evil.php
bash /tmp/deploy sh /tmp/deploy (extensionless shell script)
env python /tmp/evil.py PYTHONPATH=x python3 x.py

This is the deploy-incident class (KNOWN-LIMITS 21): "hands control to a local script the gate cannot read" — one spelling away from source /tmp/deploy.sh, which gates today.

The fix

Recognize the interpreter, not the file suffix. When a segment hands a local file to a known script interpreter (python|python3|node|nodejs|ruby|perl|php|bash|zsh|dash|ksh|sh|pwsh|powershell, after optional inline env assignments and env), opaque-exec fires — a PreToolUse boundary, exactly like ./deploy.sh.

Inline-code flags stay free by design: -c/-e/-m/-r/--code/--eval/--module and their operands are in the command string, visible to every other matcher, so opaque-exec need not own them. The matcher skips ordinary option flags (bash --posix file) then requires a file token.

Failure modes answered (acceptance #3)

  • Options before the file: bash --posix /tmp/deploy → gates (flags skipped, file remains)
  • Detached/REPL interpreter: bare python → free (no file)
  • Flags only: python --version, node -v → free
  • Inline code: bash -c "...", python -c "...", node -e "...", python -m http.server → free (code visible in-command)
  • which python, echo python /tmp/x.py → free (interpreter not in command position)

Quietness (acceptance #4)

This tightens the gate — it does not quieten it. What it now misses is declared, not hidden: a bare direct execution of an extensionless file (./deploy) could be a compiled binary, and a string matcher cannot tell an ELF from a shebang without reading the file. That class would need a C3-style context resolver (file sniff), which is deliberately out of scope here; the interpreter class is string-distinguishable and is the one this PR closes.

Tests

test/policy-opaque-exec-interpreter.test.js — 24 cases.

  • Fail-first proven: against the unpatched matcher, 11 "must gate" cases fail; the 13 "must not gate" cases pass.
  • After the fix: 24/24 pass.
  • Full policy/selfmod/git-context suites: 235/235 pass (includes egress-query-string, push-implicit-protected, opaque-exec-sep, destructive-dotdot).

SCRIPT_EXT recognizes a script by its file's extension (.ps1/.sh/.bash/
.zsh/.bat/.cmd). A local script with any other spelling flows free even
though its contents are exactly as unreadable to the gate: python
/tmp/evil.py, node /tmp/evil.js, and an extensionless script through a
shell interpreter (bash /tmp/deploy) all execute unreadable code with
no receipt.
This closes the class by recognizing the interpreter, not the file
suffix: when a segment hands a local file to a known script interpreter
(python/node/ruby/perl/php/bash/zsh/dash/sh/pwsh/powershell, after
optional env assignments), opaque-exec fires. Inline-code flags
(-c/-e/-m/-r/--eval/--module) and their operands stay free: the code
string is IN the command, visible to every other matcher.
Boundaries held: bash -c, python -c, node -e, python -m, python
--version, node -v, ls, cat, which python, echo python ... all remain
free (verified in test). Regression test proves fail-first: 11 of the
gate cases fail against the unpatched matcher, all pass after (24/24).
Derived from KNOWN-LIMITS 21 (the deploy-incident class): the gate
sees the interpreter and the file, not the code inside. One spelling
away from ./deploy.sh, which already gates.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nicolasesanchez50@githubscum