This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/sandbox/unicode_scan.go
Jeffrey Smith 7155aaf663
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m35s
CI/CD / test-sqlite (push) Successful in 2m49s
CI/CD / build-and-deploy (push) Successful in 1m21s
Feat v0.5.1 chat core (#31)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-30 12:42:54 +00:00

155 lines
4.6 KiB
Go

// Package sandbox — unicode_scan.go
//
// v0.5.1: Invisible Unicode scanning gate.
//
// Defends against GlassWorm (variation selector payloads), Trojan Source
// (bidi override attacks / CVE-2021-42574), and other invisible Unicode
// smuggling techniques.
//
// Two public functions:
// ScanSource(source, filename) → []Finding
// Verdict(findings) → (blocked bool, reason string)
//
// No dependencies beyond stdlib (unicode, fmt, strings).
// Single-pass rune iteration. No regex.
package sandbox
import (
"fmt"
"strings"
"unicode"
)
// invisibleRanges defines the Unicode ranges we scan for, grouped by category.
// Uses unicode.RangeTable with R16/R32 slices for efficient lookup.
var invisibleRanges = map[string]*unicode.RangeTable{
"zero_width": {
R16: []unicode.Range16{
{Lo: 0x200B, Hi: 0x200F, Stride: 1}, // zero-width space, joiners, bidi marks
{Lo: 0x2060, Hi: 0x2064, Stride: 1}, // word joiner, invisible operators
{Lo: 0xFEFF, Hi: 0xFEFF, Stride: 1}, // BOM / zero-width no-break space
},
},
"bidi_override": {
R16: []unicode.Range16{
{Lo: 0x202A, Hi: 0x202E, Stride: 1}, // bidi overrides (Trojan Source)
{Lo: 0x2066, Hi: 0x2069, Stride: 1}, // bidi isolates
},
},
"variation_selector": {
R16: []unicode.Range16{
{Lo: 0xFE00, Hi: 0xFE0F, Stride: 1}, // variation selectors (GlassWorm primary)
},
R32: []unicode.Range32{
{Lo: 0xE0100, Hi: 0xE01EF, Stride: 1}, // variation selector supplement (GlassWorm secondary)
},
},
"tag_character": {
R32: []unicode.Range32{
{Lo: 0xE0001, Hi: 0xE007F, Stride: 1}, // tag characters
},
},
"other_invisible": {
R16: []unicode.Range16{
{Lo: 0x3164, Hi: 0x3164, Stride: 1}, // Hangul filler
{Lo: 0xFFF9, Hi: 0xFFFB, Stride: 1}, // interlinear annotations
},
},
}
// Finding records a single invisible/deceptive Unicode character found in source.
type Finding struct {
Offset int // byte offset in source
Line int // 1-based line number
Rune rune // the character found
Category string // one of: variation_selector, bidi_override, zero_width, tag_character, other_invisible
}
// ScanSource scans source code for invisible/deceptive Unicode characters.
// Returns a slice of findings (empty if clean). Single-pass rune iteration.
func ScanSource(source string, filename string) []Finding {
var findings []Finding
line := 1
offset := 0
for _, r := range source {
for cat, rt := range invisibleRanges {
if unicode.Is(rt, r) {
findings = append(findings, Finding{
Offset: offset,
Line: line,
Rune: r,
Category: cat,
})
break
}
}
if r == '\n' {
line++
}
offset += len(string(r))
}
return findings
}
// CategoryCounts returns a map of category → count from a slice of findings.
func CategoryCounts(findings []Finding) map[string]int {
counts := make(map[string]int)
for _, f := range findings {
counts[f.Category]++
}
return counts
}
// Verdict evaluates findings and returns whether the source should be blocked.
//
// Blocking rules:
// - Any bidi_override → block (Trojan Source, zero legitimate use in Starlark)
// - variation_selector + tag_character >= 10 → block (GlassWorm signature)
// - zero_width >= 6 → block
//
// Returns blocked=false, reason="" if clean or below threshold.
func Verdict(findings []Finding) (blocked bool, reason string) {
if len(findings) == 0 {
return false, ""
}
counts := CategoryCounts(findings)
// Rule 1: Any bidi override → always block (Trojan Source / CVE-2021-42574)
if counts["bidi_override"] > 0 {
return true, fmt.Sprintf(
"blocked: %d bidirectional override character(s) detected (Trojan Source / CVE-2021-42574). "+
"These characters can reverse the visual display order of code, hiding malicious logic.",
counts["bidi_override"],
)
}
// Rule 2: GlassWorm signature — variation selectors + tag characters >= 10
glasswormCount := counts["variation_selector"] + counts["tag_character"]
if glasswormCount >= 10 {
return true, fmt.Sprintf(
"blocked: %d variation selector / tag characters detected (GlassWorm pattern). "+
"Legitimate emoji use requires 1-2; %d suggests encoded hidden payload.",
glasswormCount, glasswormCount,
)
}
// Rule 3: Excessive zero-width characters
if counts["zero_width"] >= 6 {
return true, fmt.Sprintf(
"blocked: %d zero-width characters detected. "+
"This exceeds the safe threshold and may indicate hidden content.",
counts["zero_width"],
)
}
// Below threshold — not blocked
var parts []string
for cat, n := range counts {
parts = append(parts, fmt.Sprintf("%d %s", n, cat))
}
return false, strings.Join(parts, ", ")
}