mirror of
https://github.com/bigbeartechworld/big-bear-scripts.git
synced 2026-08-04 19:15:12 -04:00
* 🔍 feat(unicode-scanner): Add binary file scanning option Enhance Unicode security scanner with optional binary file scanning: - Implement `--include-binary` flag to scan binary files - Add comprehensive binary file detection logic - Update help text and version number - Improve file type detection using file command and extensions * 🔧 refactor: Improve variable declaration in run.sh Separate variable declaration and assignment for better readability and adherence to shellcheck recommendations. This change ensures clearer code structure and potential improved static analysis compatibility.
755 lines
29 KiB
Bash
Executable File
755 lines
29 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Unicode Security Scanner v2.1.0 AI+
|
|
# Detects dangerous Unicode characters that can be used in security attacks
|
|
# Including Trojan Source attacks (CVE-2021-42574) and other invisible characters
|
|
|
|
# Script configuration
|
|
VERSION="2.1.1"
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# Command-line options (defaults)
|
|
QUIET_MODE=false
|
|
JSON_OUTPUT=false
|
|
SEVERITY_FILTER=""
|
|
ALLOWLIST_FILE="${SCRIPT_DIR}/.unicode-allowlist"
|
|
EXCLUDE_EMOJIS=false
|
|
EXCLUDE_COMMON_UNICODE=false
|
|
INCLUDE_BINARY=false
|
|
|
|
# Check dependencies
|
|
check_dependencies() {
|
|
local missing=()
|
|
command -v hexdump >/dev/null || missing+=("hexdump")
|
|
command -v grep >/dev/null || missing+=("grep")
|
|
command -v file >/dev/null || missing+=("file")
|
|
command -v find >/dev/null || missing+=("find")
|
|
|
|
if [ ${#missing[@]} -gt 0 ]; then
|
|
echo "Error: Missing required commands: ${missing[*]}" >&2
|
|
echo "Please install the required tools and try again." >&2
|
|
exit 2
|
|
fi
|
|
}
|
|
|
|
# Show help
|
|
show_help() {
|
|
cat << EOF
|
|
Unicode Security Scanner v${VERSION} - AI Enhanced with False Positive Fix
|
|
|
|
USAGE:
|
|
$0 [OPTIONS] <file|directory>
|
|
|
|
OPTIONS:
|
|
--help, -h Show this help message
|
|
--version, -v Show version information
|
|
--quiet, -q Suppress non-error output (for CI/CD)
|
|
--json Output results in JSON format
|
|
--severity LEVEL Filter by severity: critical, high, medium, low
|
|
(comma-separated, e.g., "critical,high")
|
|
--allowlist FILE Path to allowlist file (default: .unicode-allowlist)
|
|
--exclude-emojis Exclude emoji characters and variation selectors (reduces false positives)
|
|
--exclude-common Exclude common Unicode like smart quotes, dashes (very permissive)
|
|
--include-binary Include binary files (archives, images, executables, etc.)
|
|
By default, only text files are scanned to avoid false positives
|
|
|
|
EXAMPLES:
|
|
$0 ./src/ # Scan directory (text files only)
|
|
$0 script.py # Scan single file
|
|
$0 --quiet --json ./app/ > results.json # JSON output for CI
|
|
$0 --severity critical,high ./ # Only show critical/high
|
|
$0 --exclude-emojis ./ui/ # Skip emoji characters in UI code
|
|
$0 --exclude-common ./docs/ # Very permissive for documentation
|
|
$0 --include-binary ./ # Scan all files including binaries
|
|
|
|
EXIT CODES:
|
|
0 - No threats detected
|
|
1 - Threats detected
|
|
2 - Error or invalid usage
|
|
|
|
MORE INFO:
|
|
https://github.com/bigbeartechworld/big-bear-scripts
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
# Show version
|
|
show_version() {
|
|
echo "Unicode Security Scanner v${VERSION}"
|
|
exit 0
|
|
}
|
|
|
|
# Load allowlist (Unicode codes to ignore)
|
|
load_allowlist() {
|
|
# Store allowlisted codes in a simple variable (space-separated)
|
|
ALLOWLIST_CODES=""
|
|
if [ -f "$ALLOWLIST_FILE" ]; then
|
|
while IFS= read -r line; do
|
|
# Skip comments and empty lines
|
|
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
|
|
# Extract Unicode code (e.g., U+200B or 200B)
|
|
code=$(echo "$line" | grep -oE 'U\+[0-9A-Fa-f]+|^[0-9A-Fa-f]+' | tr -d 'U+' | tr '[:lower:]' '[:upper:]')
|
|
[ -n "$code" ] && ALLOWLIST_CODES="$ALLOWLIST_CODES $code "
|
|
done < "$ALLOWLIST_FILE"
|
|
fi
|
|
}
|
|
|
|
# Check if Unicode code is in allowlist
|
|
is_allowed() {
|
|
local code=$1
|
|
[[ "$ALLOWLIST_CODES" == *" $code "* ]]
|
|
}
|
|
|
|
# Check if pattern is emoji-related
|
|
is_emoji_pattern() {
|
|
local unicode_code=$1
|
|
# Emoji variation selectors (FE00-FE0F)
|
|
[[ "$unicode_code" =~ ^FE0[0-9A-F]$ ]] && return 0
|
|
# Emoji tag characters (1F3F0-1F3FA, E0020-E007F)
|
|
[[ "$unicode_code" =~ ^1F3F[0-9A-F]$ ]] && return 0
|
|
[[ "$unicode_code" =~ ^E00[2-7][0-9A-F]$ ]] && return 0
|
|
# Zero-width joiner (used in emoji sequences)
|
|
[[ "$unicode_code" == "200D" ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
# Check if pattern is common Unicode (quotes, dashes, etc.)
|
|
is_common_unicode() {
|
|
local unicode_code=$1
|
|
# Smart quotes: U+2018, U+2019, U+201C, U+201D
|
|
[[ "$unicode_code" =~ ^201[89CD]$ ]] && return 0
|
|
# Dashes: U+2010-U+2015 (hyphen, non-breaking hyphen, figure dash, en-dash, em-dash, horizontal bar)
|
|
[[ "$unicode_code" =~ ^201[0-5]$ ]] && return 0
|
|
# Ellipsis: U+2026
|
|
[[ "$unicode_code" == "2026" ]] && return 0
|
|
# Common spaces (but not zero-width): U+2007-U+200A
|
|
[[ "$unicode_code" =~ ^200[7-9A]$ ]] && return 0
|
|
# Angle quotation marks: U+2039, U+203A
|
|
[[ "$unicode_code" =~ ^203[9A]$ ]] && return 0
|
|
# Per mille: U+2030
|
|
[[ "$unicode_code" == "2030" ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
# Check if file is binary (non-text)
|
|
is_binary_file() {
|
|
local file=$1
|
|
|
|
# Check by file extension first (faster)
|
|
case "${file##*.}" in
|
|
# Archives
|
|
jar|war|ear|zip|tar|gz|tgz|bz2|xz|7z|rar|iso)
|
|
return 0
|
|
;;
|
|
# Images
|
|
jpg|jpeg|png|gif|bmp|ico|svg|webp|tiff|tif|psd)
|
|
return 0
|
|
;;
|
|
# Videos
|
|
mp4|avi|mov|wmv|flv|mkv|webm|m4v)
|
|
return 0
|
|
;;
|
|
# Audio
|
|
mp3|wav|ogg|flac|aac|wma|m4a)
|
|
return 0
|
|
;;
|
|
# Executables and libraries
|
|
exe|dll|so|dylib|a|o|class|pyc|pyo|beam)
|
|
return 0
|
|
;;
|
|
# Documents (binary formats)
|
|
pdf|doc|docx|xls|xlsx|ppt|pptx|odt|ods|odp)
|
|
return 0
|
|
;;
|
|
# Fonts
|
|
ttf|otf|woff|woff2|eot)
|
|
return 0
|
|
;;
|
|
# Databases
|
|
db|sqlite|sqlite3)
|
|
return 0
|
|
;;
|
|
# Other binary formats
|
|
bin|dat|pkg|deb|rpm|dmg)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# Use file command to check MIME type
|
|
local mime_type
|
|
mime_type=$(file -b --mime-type "$file" 2>/dev/null)
|
|
|
|
# Check if MIME type indicates binary
|
|
case "$mime_type" in
|
|
application/x-executable|\
|
|
application/x-sharedlib|\
|
|
application/x-object|\
|
|
application/x-archive|\
|
|
application/zip|\
|
|
application/gzip|\
|
|
application/x-tar|\
|
|
application/x-bzip2|\
|
|
application/x-xz|\
|
|
application/java-archive|\
|
|
application/pdf|\
|
|
application/octet-stream|\
|
|
image/*|\
|
|
video/*|\
|
|
audio/*|\
|
|
font/*)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# Check if file command says it's binary
|
|
# Note: Avoid matching "shell script text executable" - we want actual binary executables
|
|
local file_desc
|
|
file_desc=$(file "$file" 2>/dev/null)
|
|
if echo "$file_desc" | grep -qiE "executable.*binary|archive|compressed|image data|audio|video"; then
|
|
return 0
|
|
fi
|
|
|
|
# Exclude shell scripts and other text-based executables
|
|
if echo "$file_desc" | grep -qi "text"; then
|
|
return 1
|
|
fi
|
|
|
|
# Not binary
|
|
return 1
|
|
}
|
|
|
|
# Check if hex pattern appears in an emoji context
|
|
is_in_emoji_context() {
|
|
local hex_content=$1
|
|
local pattern_spaced=$2
|
|
|
|
# Look for emoji range characters (1F300-1F9FF) near the pattern
|
|
# Emoji base characters: f09f8c80 to f09fa7bf (approximate)
|
|
if echo "$hex_content" | grep -Eq "f0 9f [8-9a][0-9a-f] [0-9a-f]{2}.*$pattern_spaced"; then
|
|
return 0
|
|
fi
|
|
if echo "$hex_content" | grep -Eq "$pattern_spaced.*f0 9f [8-9a][0-9a-f] [0-9a-f]{2}"; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# List of dangerous Unicode characters as hex patterns for grep
|
|
# Format: "hex_pattern:unicode_code:description"
|
|
harmful_patterns=(
|
|
# Zero-width and invisible characters
|
|
"e2808b:200B:Zero Width Space"
|
|
"e2808c:200C:Zero Width Non-Joiner"
|
|
"e2808d:200D:Zero Width Joiner"
|
|
"e281a0:2060:Word Joiner"
|
|
"e281a1:2061:Function Application"
|
|
"e281a2:2062:Invisible Times"
|
|
"e281a3:2063:Invisible Separator"
|
|
"e281a4:2064:Invisible Plus"
|
|
"efbbbf:FEFF:Zero Width No-Break Space (BOM)"
|
|
"cd8f:034F:Combining Grapheme Joiner"
|
|
|
|
# Bidirectional text controls (Trojan Source attacks - CVE-2021-42574)
|
|
"e280aa:202A:Left-to-Right Embedding"
|
|
"e280ab:202B:Right-to-Left Embedding"
|
|
"e280ac:202C:Pop Directional Formatting"
|
|
"e280ad:202D:Left-to-Right Override"
|
|
"e280ae:202E:Right-to-Left Override"
|
|
"e281a6:2066:Left-to-Right Isolate"
|
|
"e281a7:2067:Right-to-Left Isolate"
|
|
"e281a8:2068:First Strong Isolate"
|
|
"e281a9:2069:Pop Directional Isolate"
|
|
"d89c:061C:Arabic Letter Mark"
|
|
"e2808e:200E:Left-to-Right Mark"
|
|
"e2808f:200F:Right-to-Left Mark"
|
|
|
|
# Annotation and formatting characters
|
|
"efbfb9:FFF9:Interlinear Annotation Anchor"
|
|
"efbfba:FFFA:Interlinear Annotation Separator"
|
|
"efbfbb:FFFB:Interlinear Annotation Terminator"
|
|
"efbfbc:FFFC:Object Replacement Character"
|
|
"efbfbd:FFFD:Replacement Character"
|
|
|
|
# Line and paragraph separators
|
|
"e280a8:2028:Line Separator"
|
|
"e280a9:2029:Paragraph Separator"
|
|
|
|
# Additional format characters
|
|
"c2ad:00AD:Soft Hyphen"
|
|
"e1859f:115F:Hangul Choseong Filler"
|
|
"e185a0:1160:Hangul Jungseong Filler"
|
|
"e19eb4:17B4:Khmer Vowel Inherent Aq"
|
|
"e19eb5:17B5:Khmer Vowel Inherent Aa"
|
|
"e1a08e:180E:Mongolian Vowel Separator"
|
|
"e385a4:3164:Hangul Filler"
|
|
|
|
# Variation selectors (can change character appearance)
|
|
"efe000:FE00:Variation Selector-1"
|
|
"efe001:FE01:Variation Selector-2"
|
|
"efe002:FE02:Variation Selector-3"
|
|
"efe003:FE03:Variation Selector-4"
|
|
"efe004:FE04:Variation Selector-5"
|
|
"efe005:FE05:Variation Selector-6"
|
|
"efe006:FE06:Variation Selector-7"
|
|
"efe007:FE07:Variation Selector-8"
|
|
"efe008:FE08:Variation Selector-9"
|
|
"efe009:FE09:Variation Selector-10"
|
|
"efe00a:FE0A:Variation Selector-11"
|
|
"efe00b:FE0B:Variation Selector-12"
|
|
"efe00c:FE0C:Variation Selector-13"
|
|
"efe00d:FE0D:Variation Selector-14"
|
|
"efe00e:FE0E:Variation Selector-15"
|
|
"efe00f:FE0F:Variation Selector-16"
|
|
|
|
# Homograph attack characters - Cyrillic lookalikes (CVE-2017-5116)
|
|
"d0b0:0430:Cyrillic Small Letter A (looks like Latin a)"
|
|
"d181:0441:Cyrillic Small Letter Es (looks like Latin c)"
|
|
"d0b5:0435:Cyrillic Small Letter Ie (looks like Latin e)"
|
|
"d0be:043E:Cyrillic Small Letter O (looks like Latin o)"
|
|
"d180:0440:Cyrillic Small Letter Er (looks like Latin p)"
|
|
"d185:0445:Cyrillic Small Letter Ha (looks like Latin x)"
|
|
"d183:0443:Cyrillic Small Letter U (looks like Latin y)"
|
|
"d0b2:0432:Cyrillic Small Letter Ve (looks like Latin B)"
|
|
"d096:0456:Cyrillic Small Letter Byelorussian-Ukrainian I (looks like Latin i)"
|
|
"d098:0458:Cyrillic Small Letter Je (looks like Latin j)"
|
|
"d195:0475:Cyrillic Small Letter Izhitsa (looks like Latin v)"
|
|
"d0b4:0434:Cyrillic Small Letter De (looks like Latin g in italic)"
|
|
"d197:0457:Cyrillic Small Letter Yi (looks like Latin i with dots)"
|
|
"d281:04bb:Cyrillic Small Letter Shha (looks like Latin h)"
|
|
"d4b1:0531:Armenian Capital Letter Ayb (looks like Latin U)"
|
|
"d587:0587:Armenian Small Ligature Ech Yiwn (looks like Latin w)"
|
|
|
|
# Cyrillic capital letters
|
|
"d090:0410:Cyrillic Capital Letter A (looks like Latin A)"
|
|
"d092:0412:Cyrillic Capital Letter Ve (looks like Latin B)"
|
|
"d0a1:0421:Cyrillic Capital Letter Es (looks like Latin C)"
|
|
"d095:0415:Cyrillic Capital Letter Ie (looks like Latin E)"
|
|
"d09d:041D:Cyrillic Capital Letter En (looks like Latin H)"
|
|
"d096:0406:Cyrillic Capital Letter Byelorussian-Ukrainian I (looks like Latin I)"
|
|
"d098:0408:Cyrillic Capital Letter Je (looks like Latin J)"
|
|
"d09a:041A:Cyrillic Capital Letter Ka (looks like Latin K)"
|
|
"d09c:041C:Cyrillic Capital Letter Em (looks like Latin M)"
|
|
"d09e:041E:Cyrillic Capital Letter O (looks like Latin O)"
|
|
"d0a0:0420:Cyrillic Capital Letter Er (looks like Latin P)"
|
|
"d085:0405:Cyrillic Capital Letter Dze (looks like Latin S)"
|
|
"d0a2:0422:Cyrillic Capital Letter Te (looks like Latin T)"
|
|
"d0a5:0425:Cyrillic Capital Letter Ha (looks like Latin X)"
|
|
"d0a3:0423:Cyrillic Capital Letter U (looks like Latin Y)"
|
|
|
|
# Greek lookalikes
|
|
"cebf:03BF:Greek Small Letter Omicron (looks like Latin o)"
|
|
"ceb1:03B1:Greek Small Letter Alpha (looks like Latin a in italic)"
|
|
"ceb5:03B5:Greek Small Letter Epsilon (looks like Latin e)"
|
|
"cebd:03BD:Greek Small Letter Nu (looks like Latin v)"
|
|
"cf81:03C1:Greek Small Letter Rho (looks like Latin p)"
|
|
"cf84:03C4:Greek Small Letter Tau (looks like Latin t)"
|
|
"cf85:03C5:Greek Small Letter Upsilon (looks like Latin u)"
|
|
"cf87:03C7:Greek Small Letter Chi (looks like Latin x)"
|
|
"ceb9:03B9:Greek Small Letter Iota (looks like Latin i)"
|
|
"ceba:03BA:Greek Small Letter Kappa (looks like Latin k)"
|
|
"ceb7:03B7:Greek Small Letter Eta (looks like Latin n)"
|
|
"cf89:03C9:Greek Small Letter Omega (looks like Latin w)"
|
|
|
|
# Armenian lookalikes
|
|
"d5b8:0578:Armenian Small Letter Vo (looks like Latin n)"
|
|
"d5bd:057D:Armenian Small Letter Seh (looks like Latin s)"
|
|
"d5b5:0575:Armenian Small Letter Yi (looks like Latin j)"
|
|
"d5b0:0570:Armenian Small Letter Ho (looks like Latin h)"
|
|
"d5b4:0574:Armenian Small Letter Men (looks like Latin q)"
|
|
"d5b1:0571:Armenian Small Letter Sha (looks like Latin g)"
|
|
"d5a1:0561:Armenian Small Letter Ayb (looks like Latin a)"
|
|
|
|
# Thai lookalikes (modern simplified fonts)
|
|
"e0b884:0E04:Thai Character Kho Khwai (looks like Latin A)"
|
|
"e0b897:0E17:Thai Character Tho Thahan (looks like Latin n)"
|
|
"e0b899:0E19:Thai Character No Nu (looks like Latin u)"
|
|
"e0b89a:0E1A:Thai Character Bo Baimai (looks like Latin U)"
|
|
"e0b89b:0E1B:Thai Character Po Pla (looks like Latin J)"
|
|
"e0b89e:0E1E:Thai Character Pho Phung (looks like Latin W)"
|
|
"e0b8a3:0E23:Thai Character Ro Rua (looks like Latin S)"
|
|
"e0b8a5:0E25:Thai Character Lo Ling (looks like Latin a)"
|
|
|
|
# Mathematical Alphanumeric Symbols (often used in AI attacks)
|
|
"f09d9482:1D502:Mathematical Fraktur Small A"
|
|
"f09d94b8:1D4B8:Mathematical Script Small A"
|
|
"f09d95b6:1D576:Mathematical Bold Fraktur Small A"
|
|
"f09d96ba:1D6BA:Mathematical Bold Small Alpha"
|
|
"f09d90ae:1D42E:Mathematical Bold Small A"
|
|
"f09d91b6:1D476:Mathematical Bold Italic Small A"
|
|
"f09d92aa:1D4AA:Mathematical Sans-Serif Small A"
|
|
"f09d93b2:1D4F2:Mathematical Sans-Serif Bold Small A"
|
|
"f09d94ba:1D4BA:Mathematical Sans-Serif Italic Small A"
|
|
"f09d95c2:1D582:Mathematical Sans-Serif Bold Italic Small A"
|
|
"f09d96ba:1D6BA:Mathematical Monospace Small A"
|
|
|
|
# Fullwidth characters (used in prompt injection)
|
|
"efbca1:FF21:Fullwidth Latin Capital Letter A"
|
|
"efbca2:FF22:Fullwidth Latin Capital Letter B"
|
|
"efbca3:FF23:Fullwidth Latin Capital Letter C"
|
|
"efbcb1:FF31:Fullwidth Latin Small Letter A"
|
|
"efbcb2:FF32:Fullwidth Latin Small Letter B"
|
|
"efbcb3:FF33:Fullwidth Latin Small Letter C"
|
|
|
|
# Number Forms that can be confused with letters
|
|
"e285a0:2160:Roman Numeral One (looks like Latin I)"
|
|
"e285a5:2165:Roman Numeral Six (looks like VI)"
|
|
"e285a9:2169:Roman Numeral Ten (looks like Latin X)"
|
|
"e285b4:2174:Small Roman Numeral Five (looks like Latin v)"
|
|
"e285b9:2179:Small Roman Numeral Ten (looks like Latin x)"
|
|
|
|
# AI-specific prompt injection patterns
|
|
"e2819f:205F:Medium Mathematical Space (invisible separator)"
|
|
"e28087:2007:Figure Space (numeric space manipulation)"
|
|
"e28088:2008:Punctuation Space (can break tokenization)"
|
|
"e28089:2009:Thin Space (subtle spacing attack)"
|
|
"e2808a:200A:Hair Space (micro-spacing attack)"
|
|
"e2808b:202F:Narrow No-Break Space (line manipulation)"
|
|
"e281a5:2065:Inhibit Arabic Form Shaping (script confusion)"
|
|
"e281a6:2066:Left-to-Right Isolate (directional confusion)"
|
|
"e281a7:2067:Right-to-Left Isolate (directional confusion)"
|
|
"e281a8:2068:First Strong Isolate (directional confusion)"
|
|
"e281a9:2069:Pop Directional Isolate (directional confusion)"
|
|
|
|
# Unicode normalization attack vectors
|
|
"cc80:0300:Combining Grave Accent (normalization attack)"
|
|
"cc81:0301:Combining Acute Accent (normalization attack)"
|
|
"cc82:0302:Combining Circumflex Accent (normalization attack)"
|
|
"cc83:0303:Combining Tilde (normalization attack)"
|
|
"cc84:0304:Combining Macron (normalization attack)"
|
|
"cc88:0308:Combining Diaeresis (normalization attack)"
|
|
"cc8a:030A:Combining Ring Above (normalization attack)"
|
|
"cc8c:030C:Combining Caron (normalization attack)"
|
|
|
|
# Confusable punctuation and symbols
|
|
"e28098:2018:Left Single Quotation Mark (looks like apostrophe)"
|
|
"e28099:2019:Right Single Quotation Mark (looks like apostrophe)"
|
|
"e2809c:201C:Left Double Quotation Mark (looks like quote)"
|
|
"e2809d:201D:Right Double Quotation Mark (looks like quote)"
|
|
"e28090:2010:Hyphen (different from ASCII hyphen)"
|
|
"e28091:2011:Non-Breaking Hyphen (different from ASCII hyphen)"
|
|
"e28092:2012:Figure Dash (looks like hyphen)"
|
|
"e28093:2013:En Dash (looks like hyphen)"
|
|
"e28094:2014:Em Dash (looks like double hyphen)"
|
|
"e28095:2015:Horizontal Bar (looks like long dash)"
|
|
"e280a6:2026:Horizontal Ellipsis (looks like three dots)"
|
|
"e280b0:2030:Per Mille Sign (looks like percent)"
|
|
"e280b9:2039:Single Left-Pointing Angle Quotation Mark"
|
|
"e280ba:203A:Single Right-Pointing Angle Quotation Mark"
|
|
|
|
# Additional zero-width and control characters
|
|
"efbfb0:FFF0:Reserved Character (potential bypass)"
|
|
"efbfb1:FFF1:Reserved Character (potential bypass)"
|
|
"efbfb2:FFF2:Reserved Character (potential bypass)"
|
|
"efbfb3:FFF3:Reserved Character (potential bypass)"
|
|
"efbfb4:FFF4:Reserved Character (potential bypass)"
|
|
"efbfb5:FFF5:Reserved Character (potential bypass)"
|
|
"efbfb6:FFF6:Reserved Character (potential bypass)"
|
|
"efbfb7:FFF7:Reserved Character (potential bypass)"
|
|
"efbfb8:FFF8:Reserved Character (potential bypass)"
|
|
|
|
# Superscript and subscript characters (AI confusion)
|
|
"e281b0:2070:Superscript Zero"
|
|
"c2b9:00B9:Superscript One"
|
|
"c2b2:00B2:Superscript Two"
|
|
"c2b3:00B3:Superscript Three"
|
|
"e281b4:2074:Superscript Four"
|
|
"e281b5:2075:Superscript Five"
|
|
"e281b6:2076:Superscript Six"
|
|
"e281b7:2077:Superscript Seven"
|
|
"e281b8:2078:Superscript Eight"
|
|
"e281b9:2079:Superscript Nine"
|
|
"e28280:2080:Subscript Zero"
|
|
"e28281:2081:Subscript One"
|
|
"e28282:2082:Subscript Two"
|
|
"e28283:2083:Subscript Three"
|
|
"e28284:2084:Subscript Four"
|
|
|
|
# Emoji variation selectors and modifiers (can hide content)
|
|
"f09f8fb0:1F3F0:Emoji Tag Latin Small Letter P"
|
|
"f09f8fb1:1F3F1:Emoji Tag Latin Small Letter Q"
|
|
"f09f8fb2:1F3F2:Emoji Tag Latin Small Letter R"
|
|
"f09f8fb3:1F3F3:Emoji Tag Latin Small Letter S"
|
|
"f09f8fb4:1F3F4:Emoji Tag Latin Small Letter T"
|
|
"f09f8fb5:1F3F5:Emoji Tag Latin Small Letter U"
|
|
"f09f8fb6:1F3F6:Emoji Tag Latin Small Letter V"
|
|
"f09f8fb7:1F3F7:Emoji Tag Latin Small Letter W"
|
|
"f09f8fb8:1F3F8:Emoji Tag Latin Small Letter X"
|
|
"f09f8fb9:1F3F9:Emoji Tag Latin Small Letter Y"
|
|
"f09f8fba:1F3FA:Emoji Tag Latin Small Letter Z"
|
|
|
|
# CJK Compatibility characters (can mimic ASCII)
|
|
"efbc81:FF01:Fullwidth Exclamation Mark"
|
|
"efbc9f:FF1F:Fullwidth Question Mark"
|
|
"efbc8a:FF0A:Fullwidth Asterisk"
|
|
"efbc8b:FF0B:Fullwidth Plus Sign"
|
|
"efbc8d:FF0D:Fullwidth Hyphen-Minus"
|
|
"efbc8e:FF0E:Fullwidth Full Stop"
|
|
"efbc8f:FF0F:Fullwidth Solidus"
|
|
"efbc9a:FF1A:Fullwidth Colon"
|
|
"efbc9b:FF1B:Fullwidth Semicolon"
|
|
"efbc9c:FF1C:Fullwidth Less-Than Sign"
|
|
"efbc9d:FF1D:Fullwidth Equals Sign"
|
|
"efbc9e:FF1E:Fullwidth Greater-Than Sign"
|
|
)
|
|
|
|
# Check dependencies first
|
|
check_dependencies
|
|
|
|
# Parse command-line arguments
|
|
target=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--help|-h)
|
|
show_help
|
|
;;
|
|
--version|-v)
|
|
show_version
|
|
;;
|
|
--quiet|-q)
|
|
QUIET_MODE=true
|
|
shift
|
|
;;
|
|
--json)
|
|
JSON_OUTPUT=true
|
|
shift
|
|
;;
|
|
--severity)
|
|
SEVERITY_FILTER="$2"
|
|
shift 2
|
|
;;
|
|
--allowlist)
|
|
ALLOWLIST_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--exclude-emojis)
|
|
EXCLUDE_EMOJIS=true
|
|
shift
|
|
;;
|
|
--exclude-common)
|
|
EXCLUDE_COMMON_UNICODE=true
|
|
shift
|
|
;;
|
|
--include-binary)
|
|
INCLUDE_BINARY=true
|
|
shift
|
|
;;
|
|
-*)
|
|
echo "Error: Unknown option: $1" >&2
|
|
echo "Use --help for usage information" >&2
|
|
exit 2
|
|
;;
|
|
*)
|
|
target="$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$target" ]; then
|
|
echo "Error: No target specified" >&2
|
|
echo "Usage: $0 [OPTIONS] <file|directory>" >&2
|
|
echo "Use --help for more information" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Load allowlist into global variable
|
|
load_allowlist
|
|
|
|
# Show header unless in quiet or JSON mode
|
|
if [ "$QUIET_MODE" = false ] && [ "$JSON_OUTPUT" = false ]; then
|
|
echo -e "\033[1;35m╔══════════════════════════════════════════════════════════════╗\033[0m"
|
|
echo -e "\033[1;35m║ Big Bear Unicode Security Scanner v2.1.1 AI+ ║\033[0m"
|
|
echo -e "\033[1;35m║ Detecting dangerous Unicode & AI injection attacks ║\033[0m"
|
|
echo -e "\033[1;35m║ Please support me! ║\033[0m"
|
|
echo -e "\033[1;35m║ https://ko-fi.com/bigbeartechworld ║\033[0m"
|
|
echo -e "\033[1;35m║ Thank you! ║\033[0m"
|
|
echo -e "\033[1;35m║ https://bigbeartechworld.com ║\033[0m"
|
|
echo -e "\033[1;35m╚══════════════════════════════════════════════════════════════╝\033[0m"
|
|
echo
|
|
fi
|
|
|
|
# Initialize counters and results for summary
|
|
total_files=0
|
|
files_with_issues=0
|
|
declare -a json_results
|
|
|
|
# Single file search function
|
|
search_file() {
|
|
file="$1"
|
|
|
|
# Skip binary files unless --include-binary is set
|
|
if [ "$INCLUDE_BINARY" = false ] && is_binary_file "$file"; then
|
|
if [ "$QUIET_MODE" = false ] && [ "$JSON_OUTPUT" = false ]; then
|
|
echo -e "\n\033[1;34mSkipping:\033[0m $file \033[90m(binary file)\033[0m"
|
|
fi
|
|
((total_files++))
|
|
return
|
|
fi
|
|
|
|
if [ "$QUIET_MODE" = false ] && [ "$JSON_OUTPUT" = false ]; then
|
|
echo -e "\n\033[1;34mScanning:\033[0m $file"
|
|
fi
|
|
|
|
# Check file encoding - be more lenient with ASCII files
|
|
# Use reliable MIME/type detection (macOS: -bI prints mime; fallback to -b)
|
|
file_info=$(file -bI "$file" 2>/dev/null || file -b "$file" 2>/dev/null)
|
|
if ! echo "$file_info" | grep -qE '(utf-8|us-ascii)'; then
|
|
if [ "$QUIET_MODE" = false ] && [ "$JSON_OUTPUT" = false ]; then
|
|
echo -e " \033[1;33mWarning:\033[0m Non-UTF8 file detected ($file_info)"
|
|
fi
|
|
fi
|
|
|
|
found_any=false
|
|
local -a file_findings
|
|
|
|
# Convert file to spaced hex bytes for pattern matching (enforces byte alignment)
|
|
# Example: "ef bb bf ..." (lowercase, space-separated)
|
|
hex_content=$(hexdump -ve '1/1 "%.2x "' "$file" 2>/dev/null)
|
|
|
|
if [ -z "$hex_content" ]; then
|
|
if [ "$QUIET_MODE" = false ] && [ "$JSON_OUTPUT" = false ]; then
|
|
echo -e " \033[1;33mWarning:\033[0m Could not read file as binary"
|
|
fi
|
|
((total_files++))
|
|
return
|
|
fi
|
|
|
|
# Search for each harmful pattern
|
|
for pattern_info in "${harmful_patterns[@]}"; do
|
|
IFS=':' read -r hex_pattern unicode_code description <<< "$pattern_info"
|
|
|
|
# Check if in allowlist
|
|
if is_allowed "$unicode_code"; then
|
|
continue
|
|
fi
|
|
|
|
# Skip emoji-related patterns if flag is set
|
|
if [ "$EXCLUDE_EMOJIS" = true ] && is_emoji_pattern "$unicode_code"; then
|
|
continue
|
|
fi
|
|
|
|
# Skip common Unicode if flag is set
|
|
if [ "$EXCLUDE_COMMON_UNICODE" = true ] && is_common_unicode "$unicode_code"; then
|
|
continue
|
|
fi
|
|
|
|
# Transform the contiguous hex pattern (e.g., "efbbbf") into space-separated bytes ("ef bb bf")
|
|
pattern_spaced=$(echo "$hex_pattern" | sed 's/../& /g; s/ $//')
|
|
|
|
# Match whole-byte sequences only: (^| )<bytes>( |$)
|
|
if echo "$hex_content" | grep -Eq "(^| )$pattern_spaced( |$)"; then
|
|
# For emoji-related characters, check context even if not excluded
|
|
if is_emoji_pattern "$unicode_code" && is_in_emoji_context "$hex_content" "$pattern_spaced"; then
|
|
continue
|
|
fi
|
|
if [ "$found_any" = false ]; then
|
|
if [ "$JSON_OUTPUT" = false ] && [ "$QUIET_MODE" = false ]; then
|
|
echo -e " \033[1;31m[!] Dangerous Unicode characters found:\033[0m"
|
|
fi
|
|
found_any=true
|
|
((files_with_issues++))
|
|
fi
|
|
|
|
# Find line numbers by searching the original file
|
|
temp_char=$(echo "$hex_pattern" | sed 's/../\\x&/g')
|
|
line_matches=$(grep -n "$(printf "$temp_char")" "$file" 2>/dev/null || echo "")
|
|
|
|
if [ "$JSON_OUTPUT" = true ]; then
|
|
# Collect for JSON output
|
|
if [ -n "$line_matches" ]; then
|
|
while IFS=':' read -r line_num line_content; do
|
|
file_findings+=("{\"unicode\":\"U+$unicode_code\",\"description\":\"$description\",\"line\":$line_num,\"content\":\"$(echo "$line_content" | sed 's/"/\\"/g')\"}")
|
|
done <<< "$line_matches"
|
|
else
|
|
file_findings+=("{\"unicode\":\"U+$unicode_code\",\"description\":\"$description\",\"line\":null,\"content\":null}")
|
|
fi
|
|
else
|
|
if [ "$QUIET_MODE" = false ]; then
|
|
echo -e " \033[1;91mU+$unicode_code\033[0m ($description)"
|
|
|
|
if [ -n "$line_matches" ]; then
|
|
echo "$line_matches" | while IFS=':' read -r line_num line_content; do
|
|
echo -e " \033[36mLine $line_num:\033[0m $line_content"
|
|
done
|
|
else
|
|
echo -e " \033[33m(Character found but line detection failed)\033[0m"
|
|
fi
|
|
echo
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [ "$found_any" = false ]; then
|
|
if [ "$QUIET_MODE" = false ] && [ "$JSON_OUTPUT" = false ]; then
|
|
echo -e " \033[1;32m✓ No dangerous Unicode characters found\033[0m"
|
|
fi
|
|
else
|
|
if [ "$JSON_OUTPUT" = true ]; then
|
|
# Add file results to JSON array
|
|
local findings_json=$(IFS=,; echo "${file_findings[*]}")
|
|
json_results+=("{\"file\":\"$file\",\"findings\":[$findings_json]}")
|
|
fi
|
|
fi
|
|
|
|
((total_files++))
|
|
}
|
|
|
|
# Handle directories recursively
|
|
if [ -d "$target" ]; then
|
|
# Validate target is a directory
|
|
if [ ! -d "$target" ]; then
|
|
echo "Error: Directory not found: $target" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Use a simpler approach - collect all files first, then process them
|
|
if [ "$QUIET_MODE" = false ] && [ "$JSON_OUTPUT" = false ]; then
|
|
echo "Collecting files..."
|
|
fi
|
|
file_list=$(find "$target" -type f 2>/dev/null)
|
|
|
|
if [ -z "$file_list" ]; then
|
|
echo "Error: No files found in $target" >&2
|
|
exit 2
|
|
fi
|
|
|
|
while IFS= read -r file; do
|
|
if [ -n "$file" ]; then
|
|
search_file "$file"
|
|
fi
|
|
done <<< "$file_list"
|
|
elif [ -f "$target" ]; then
|
|
search_file "$target"
|
|
else
|
|
echo "Error: Target not found or not accessible: $target" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Print summary
|
|
if [ "$JSON_OUTPUT" = true ]; then
|
|
# Output JSON results
|
|
results_json=$(IFS=,; echo "${json_results[*]}")
|
|
echo "{\"scanner\":\"Unicode Security Scanner\",\"version\":\"${VERSION}\",\"total_files\":$total_files,\"files_with_issues\":$files_with_issues,\"results\":[$results_json]}"
|
|
else
|
|
if [ "$QUIET_MODE" = false ]; then
|
|
echo -e "\n\033[1;35m╔══════════════════════════════════════════════════════════════╗\033[0m"
|
|
echo -e "\033[1;35m║ Summary ║\033[0m"
|
|
echo -e "\033[1;35m╚══════════════════════════════════════════════════════════════╝\033[0m"
|
|
echo -e "\033[1;36mTotal files scanned:\033[0m $total_files"
|
|
echo -e "\033[1;36mFiles with issues:\033[0m $files_with_issues"
|
|
fi
|
|
|
|
if [ $files_with_issues -eq 0 ]; then
|
|
if [ "$QUIET_MODE" = false ]; then
|
|
echo -e "\033[1;32m✓ No dangerous Unicode characters detected!\033[0m"
|
|
fi
|
|
exit 0
|
|
else
|
|
if [ "$QUIET_MODE" = false ]; then
|
|
echo -e "\033[1;31m⚠ Found dangerous Unicode characters in $files_with_issues file(s)\033[0m"
|
|
fi
|
|
exit 1
|
|
fi
|
|
fi |