Files
codebase-memory-mcp/scripts/security-strings.sh
T
Martin Vogel 02608927e4 fix(security-strings): re-review the UI bundle's password-mode lines
Smoke's binary string audit blocked on every platform in the dry run on
70543ba8 (run 38204190338, all 12 smoke jobs): "Credential patterns
found in binary". #2068 rebuilt the graph-ui bundle. The two reviewed
Monaco 0.56.0 quick-input lines are still there but minified to different
bytes, so their SHA-256 no longer matched the pair recorded on 09-09.

Re-reviewed: the CI-built bundle has exactly two flagged lines (15,644 and
72,472 bytes). Every match is the input box's password-mode flag
(_password=!1, password=!!e.password, inputBox.password=this.password,
inputBox.password=!1), not a credential. All 12 smoke logs show the same
two lines. The new pair is computed as filter_reviewed_lines does
(printf '%s' line | sha256) on CI's binaries: GNU strings on the Linux
amd64 artifact and Apple strings on the darwin arm64 artifact give the same
two hashes, and both are exempted. The comment now says how to redo this
when the bundle changes again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-12 19:11:04 +02:00

306 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Layer 2: Binary string audit — post-build check on the production binary.
#
# Scans extracted strings for:
# 1. Unauthorized URLs (only github.com + localhost allowed)
# 2. Suspiciously long base64-encoded payloads
# 3. Dangerous command names (wget, nc, netcat, telnet, ssh, /dev/tcp)
# 4. Credential patterns (password=, secret=, api_key=)
#
# Usage: scripts/security-strings.sh <binary-path>
BINARY="${1:?usage: security-strings.sh <binary-path>}"
if [[ ! -f "$BINARY" ]]; then
echo "FAIL: binary not found: $BINARY"
exit 1
fi
FAIL=0
# Detect file type. Shell scripts and other text files extract poorly via
# `strings` (the entire content becomes the "strings"), and rules tuned for
# compiled binaries (URL allowlist, wget/telnet detection) produce false
# positives — install.sh legitimately uses `wget` as a curl fallback, and
# `case` glob patterns like `https://*` look like unauthorized URLs.
#
# Strategy: for non-binary files we still run credential and base64 audits
# (those are universally meaningful), but skip the URL and dangerous-command
# audits which are designed for compiled artifacts. Script content is
# reviewed in PRs and scanned end-to-end by VirusTotal in the same pipeline.
IS_SCRIPT=false
# Structured-text artifacts by EXTENSION first: `file` reports JSON as
# "JSON data" / "JSON text data", which matched none of the text patterns
# below, so an .mcpb bundle manifest was audited as if it were a compiled
# binary and its own homepage/documentation URLs were BLOCKED as unauthorized.
# The extension check also covers hosts where file(1) is absent, where the
# `command -v` guard below silently leaves every artifact classified as binary.
case "$BINARY" in
*.json|*.jsonc|*.yaml|*.yml|*.toml|*.md|*.txt) IS_SCRIPT=true ;;
esac
if [ "$IS_SCRIPT" = false ] && command -v file &>/dev/null; then
FILE_TYPE=$(file -b "$BINARY" 2>/dev/null || echo "")
case "$FILE_TYPE" in
*"shell script"*|*"ASCII text"*|*"UTF-8 Unicode text"*|*"Unicode text"*|*"a /usr/bin/env"*|\
*"JSON"*|*"XML"*)
IS_SCRIPT=true
;;
esac
fi
if $IS_SCRIPT; then
echo "=== Layer 2: Script Content Audit ==="
else
echo "=== Layer 2: Binary String Audit ==="
fi
echo "File: $BINARY"
echo ""
# Check for strings command (needs binutils on some MSYS2 setups)
if ! command -v strings &>/dev/null; then
echo "SKIP: 'strings' command not available"
exit 0
fi
# Extract all printable strings (min length 4)
STRINGS_FILE=$(mktemp)
SEC_CMDS=$(mktemp)
SEC_CREDS=$(mktemp)
trap 'rm -f "$STRINGS_FILE" "$SEC_CMDS" "$SEC_CREDS"' EXIT
strings -n 4 "$BINARY" | sort -u > "$STRINGS_FILE"
# Remove only byte-for-byte reviewed strings from the named detector candidates.
# Hash the entire extracted line, excluding its line terminator, without trimming
# or normalizing. A rebuilt dependency that changes even one byte requires review.
# Never filter STRINGS_FILE: URL and base64 checks must still inspect these lines.
filter_reviewed_lines() {
local candidates="$1"; shift
local line digest expected allowed
local -a hash_command
if command -v sha256sum &>/dev/null; then
hash_command=(sha256sum)
elif command -v shasum &>/dev/null; then
hash_command=(shasum -a 256)
else
echo "FAIL: SHA-256 utility required to verify reviewed binary strings" >&2
return 1
fi
while IFS= read -r line; do
digest=$(printf '%s' "$line" | "${hash_command[@]}")
digest=${digest%% *}
allowed=false
for expected in "$@"; do
if [[ "$digest" == "$expected" ]]; then
allowed=true
break
fi
done
if ! $allowed; then
printf '%s\n' "$line"
fi
done < "$candidates" > "${candidates}.tmp"
mv "${candidates}.tmp" "$candidates"
}
# ── 1. URL audit (binary only — scripts handled via VT + PR review) ────
if $IS_SCRIPT; then
echo "--- URL audit (skipped — script file) ---"
else
echo "--- URL audit ---"
# Allowed URL prefixes
ALLOWED_URLS=(
"https://api.github.com/repos/DeusData/codebase-memory-mcp"
"https://github.com/DeusData/codebase-memory-mcp"
# Our own org root and documentation site: the .mcpb bundle manifest carries
# them as homepage/documentation fields, and the graph UI links the docs.
"https://github.com/DeusData"
"https://deusdata.github.io/codebase-memory-mcp"
"http://127.0.0.1"
"http://localhost"
# SQLite internal URLs (part of vendored sqlite3 strings)
"https://sqlite.org"
"https://www.sqlite.org"
# Toolchain URLs embedded by compiler/linker in static builds
"https://bugs.launchpad.net"
"https://gcc.gnu.org"
"https://sourceware.org"
# MSYS2 CLANG64 toolchain (libc++/compiler-rt) package-tracker URL, baked
# into the static Windows .exe — Windows-only, hence Linux smoke stays clean.
"https://github.com/msys2/MINGW-packages"
# W3C XML namespace URIs (SVG, MathML, XLink — used in UI bundle)
"http://www.w3.org/"
# UI bundle: React, Three.js, Tailwind, Google Fonts, bundled libraries
"https://react.dev"
"https://fonts.googleapis.com"
"https://fonts.gstatic.com"
"https://tailwindcss.com"
"https://cdn.jsdelivr.net"
"https://docs.pmnd.rs"
"https://jcgt.org"
"https://github.com/pmndrs"
"https://github.com/react-spring"
"https://github.com/101arrowz"
"https://github.com/arty-name"
"https://github.com/fredli74"
"https://github.com/lojjic"
)
# Exact documentation/license/example tokens embedded by pinned UI dependencies:
# Monaco 0.56.0, Transformers.js 4.2.0, ONNX Runtime, and the Markdown renderer.
# The URL extractor retains closing Markdown punctuation; keep those exact bytes
# too. These are references in diagnostics, editor help, and operator schemas,
# not download-host permissions. No sibling path or hostname inherits an entry.
ALLOWED_UI_REFERENCE_URLS=(
"https://web.dev/cross-origin-isolation-guide/"
"https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier)."
"https://developer.mozilla.org/en-US/docs/Web/API/Cache"
"https://github.com/huggingface/transformers.js/issues/new/choose"
"https://github.com/microsoft/vscode/issues/103170"
"https://github.com/markedjs/marked."
# Numerical-overflow reference in the ONNX attention schema.
"https://tinyurl.com/sudb9s96),"
"https://ieeexplore.ieee.org/document/1163711"
"https://code.visualstudio.com/docs/editor/codebasics#_find-and-replace)"
# Sample link insertion text in the Monaco Markdown editor.
"https://microsoft.com)"
"https://github.com/syntax-tree/hast-util-to-jsx-runtime"
"https://github.com/microsoft/vscode/issues/new"
"https://github.com/microsoft/vscode/blob/main/LICENSE.txt"
"https://github.com/microsoft/monaco-editor#faq"
"https://gist.github.com/hollance/42e32852f24243b748ae6bc1f985b13a"
"https://github.com/remarkjs/react-markdown/blob/main/changelog.md"
"https://docs.nvidia.com/cuda/cublas/index.html#cublasLtOrder_t"
"https://arxiv.org/abs/1502.03167."
)
while IFS= read -r url; do
# Skip short false positives from binary data (e.g. "https://H9")
if [[ ${#url} -lt 15 ]]; then
continue
fi
allowed=false
# The pinned, opt-in browser model uses these two verified download hosts.
# Match host boundaries; a similar hostname must not inherit permission.
case "$url" in
"https://huggingface.co"|"https://huggingface.co/"*|"https://huggingface.co;"|\
"https://us.aws.cdn.hf.co"|"https://us.aws.cdn.hf.co/"*|"https://us.aws.cdn.hf.co;")
allowed=true
;;
esac
for reference in "${ALLOWED_UI_REFERENCE_URLS[@]}"; do
if [[ "$url" == "$reference" ]]; then
allowed=true
break
fi
done
for prefix in "${ALLOWED_URLS[@]}"; do
if [[ "$url" == "$prefix"* ]]; then
allowed=true
break
fi
done
if ! $allowed; then
echo "BLOCKED: Unauthorized URL in binary: $url"
FAIL=1
fi
done < <(grep -oE 'https?://[a-zA-Z0-9._/~:@!$&()*+,;=?#%[-]+' "$STRINGS_FILE" || true)
if [[ $FAIL -eq 0 ]]; then
echo "OK: All URLs are authorized."
fi
fi # end !IS_SCRIPT URL audit
# ── 2. Base64 payload detection ──────────────────────────────────
echo ""
echo "--- Base64 payload detection ---"
# Look for base64-like strings longer than 100 chars (potential encoded payloads)
B64_COUNT=$(grep -cE '^[A-Za-z0-9+/]{100,}={0,2}$' "$STRINGS_FILE" || true)
if [[ "$B64_COUNT" -gt 0 ]]; then
echo "WARNING: Found $B64_COUNT potential base64-encoded strings > 100 chars"
grep -E '^[A-Za-z0-9+/]{100,}={0,2}$' "$STRINGS_FILE" | head -5 | while IFS= read -r s; do
echo " ${s:0:80}..."
done
# Warning only — tree-sitter grammar data can look like base64
else
echo "OK: No suspicious base64 payloads found."
fi
# ── 3. Dangerous command detection (binary only) ─────────────────
echo ""
if $IS_SCRIPT; then
echo "--- Dangerous command detection (skipped — script file) ---"
DANGEROUS_CMDS=''
else
echo "--- Dangerous command detection ---"
DANGEROUS_CMDS='wget|netcat|ncat|/dev/tcp|telnet'
# Known-benign matches (vendored grammar URI scheme tables, etc.). Each entry
# is a regex matched against the full line; matches are stripped before
# evaluation. Document the source so reviewers can verify the false positive.
ALLOWED_DANGEROUS=(
'^telnet$' # rst tree-sitter grammar: valid_schemas[] in vendored/grammars/rst/tree_sitter_rst/chars.c
)
if grep -wE "$DANGEROUS_CMDS" "$STRINGS_FILE" > "$SEC_CMDS" 2>/dev/null && [ -s "$SEC_CMDS" ]; then
for allow in "${ALLOWED_DANGEROUS[@]}"; do
grep -vE "$allow" "$SEC_CMDS" > "${SEC_CMDS}.tmp" || true
mv "${SEC_CMDS}.tmp" "$SEC_CMDS"
done
# Monaco 0.56.0 shell tokenizer: wget/telnet occur in its command keyword
# array. Reviewed full line: assets/shell-DiJ1NA_G.js:1 (2,934 bytes).
filter_reviewed_lines "$SEC_CMDS" \
0d9431238bb1665cc08548a02889077577f6a5e2f7f945ccc8c5128cefa7d688
fi
if [ -s "$SEC_CMDS" ]; then
echo "BLOCKED: Dangerous commands found in binary:"
cat "$SEC_CMDS"
FAIL=1
else
echo "OK: No dangerous commands found."
fi
fi # end !IS_SCRIPT dangerous-cmd audit
# ── 4. Credential pattern detection ──────────────────────────────
echo ""
echo "--- Credential pattern detection ---"
CRED_PATTERNS='password=|secret=|api_key=|apikey=|auth_token=|private_key='
if grep -iE "$CRED_PATTERNS" "$STRINGS_FILE" > "$SEC_CREDS" 2>/dev/null; then
# Monaco 0.56.0 quick-input password mode is a boolean/UI property, not a
# credential. Reviewed full lines of the graph-ui bundle as CI builds it
# since #2068: 15,644 and 72,472 bytes; every match is the input box's
# password-mode flag (_password=!1, password=!!e.password,
# inputBox.password=this.password, inputBox.password=!1). The same two lines
# appear on every platform. These hashes exempt no command or URL.
# A rebuilt bundle changes these bytes: the smoke log prints the lines,
# re-review them and replace both hashes (the 09-09 pair for
# assets/index-B4p3fUhm.js stopped matching with #2068).
filter_reviewed_lines "$SEC_CREDS" \
8b4df6dea02b9433ac5b7ddb9f186c0f38c9f016fa8c610aa9ef987ae4bd81cf \
e4ef9bacb65321a35ae24f154aede5ea157d7c2f7e084c1c6adcd4d33b520325
fi
if [ -s "$SEC_CREDS" ]; then
echo "BLOCKED: Credential patterns found in binary:"
cat "$SEC_CREDS"
FAIL=1
else
echo "OK: No credential patterns found."
fi
# ── Summary ──────────────────────────────────────────────────────
echo ""
if [[ $FAIL -ne 0 ]]; then
echo "=== BINARY STRING AUDIT FAILED ==="
exit 1
fi
echo "=== Binary string audit passed ==="