package sandbox import ( "context" "strings" "testing" ) // ── ScanSource ────────────────────────────── func TestScanSource_CleanStarlark(t *testing.T) { source := ` def on_request(req): path = req["path"] method = req["method"] return {"status": 200, "body": "ok"} ` findings := ScanSource(source, "clean.star") if len(findings) != 0 { t.Errorf("expected 0 findings for clean source, got %d", len(findings)) } } func TestScanSource_FewVariationSelectors(t *testing.T) { // 1-2 variation selectors — legitimate emoji use (e.g. text vs emoji presentation) source := "emoji = \"\u2764\uFE0F\" # heart with variation selector" findings := ScanSource(source, "emoji.star") if len(findings) != 1 { t.Fatalf("expected 1 finding for single VS, got %d", len(findings)) } if findings[0].Category != "variation_selector" { t.Errorf("expected category variation_selector, got %q", findings[0].Category) } if findings[0].Rune != '\uFE0F' { t.Errorf("expected rune U+FE0F, got U+%04X", findings[0].Rune) } } func TestScanSource_GlassWormPattern(t *testing.T) { // 50+ variation selectors — GlassWorm payload pattern var sb strings.Builder sb.WriteString("data = \"") for i := 0; i < 50; i++ { sb.WriteRune(rune(0xFE00 + (i % 16))) // cycle through VS1-VS16 } sb.WriteString("\"") source := sb.String() findings := ScanSource(source, "glassworm.star") count := 0 for _, f := range findings { if f.Category == "variation_selector" { count++ } } if count != 50 { t.Errorf("expected 50 variation_selector findings, got %d", count) } } func TestScanSource_BidiOverride(t *testing.T) { // RLO (U+202E) — Trojan Source attack source := "name = \"\u202Emalicious\"" findings := ScanSource(source, "bidi.star") if len(findings) != 1 { t.Fatalf("expected 1 finding, got %d", len(findings)) } if findings[0].Category != "bidi_override" { t.Errorf("expected bidi_override, got %q", findings[0].Category) } } func TestScanSource_BidiIsolate(t *testing.T) { // LRI (U+2066), PDI (U+2069) source := "text = \"\u2066hidden\u2069\"" findings := ScanSource(source, "bidi_isolate.star") bidiCount := 0 for _, f := range findings { if f.Category == "bidi_override" { bidiCount++ } } if bidiCount != 2 { t.Errorf("expected 2 bidi_override findings, got %d", bidiCount) } } func TestScanSource_ZeroWidthBelow(t *testing.T) { // 3 zero-width chars — below threshold source := "a = \"\u200B\u200B\u200B\"" findings := ScanSource(source, "zw.star") zwCount := 0 for _, f := range findings { if f.Category == "zero_width" { zwCount++ } } if zwCount != 3 { t.Errorf("expected 3 zero_width findings, got %d", zwCount) } } func TestScanSource_ZeroWidthAbove(t *testing.T) { // 8 zero-width chars — above threshold source := "a = \"\u200B\u200B\u200B\u200B\u200B\u200B\u200B\u200B\"" findings := ScanSource(source, "zw_many.star") zwCount := 0 for _, f := range findings { if f.Category == "zero_width" { zwCount++ } } if zwCount != 8 { t.Errorf("expected 8 zero_width findings, got %d", zwCount) } } func TestScanSource_TagCharacters(t *testing.T) { // Tag characters (U+E0001-U+E007F) var sb strings.Builder sb.WriteString("tag = \"") for i := 0; i < 15; i++ { sb.WriteRune(rune(0xE0001 + i)) } sb.WriteString("\"") source := sb.String() findings := ScanSource(source, "tags.star") tagCount := 0 for _, f := range findings { if f.Category == "tag_character" { tagCount++ } } if tagCount != 15 { t.Errorf("expected 15 tag_character findings, got %d", tagCount) } } func TestScanSource_HangulFiller(t *testing.T) { source := "x = \"\u3164\"" // Hangul filler findings := ScanSource(source, "hangul.star") if len(findings) != 1 { t.Fatalf("expected 1 finding, got %d", len(findings)) } if findings[0].Category != "other_invisible" { t.Errorf("expected other_invisible, got %q", findings[0].Category) } } func TestScanSource_LineTracking(t *testing.T) { source := "line1\nline2\nline3 = \"\u202E\"" findings := ScanSource(source, "lines.star") if len(findings) != 1 { t.Fatalf("expected 1 finding, got %d", len(findings)) } if findings[0].Line != 3 { t.Errorf("expected line 3, got %d", findings[0].Line) } } func TestScanSource_VariationSelectorSupplement(t *testing.T) { // U+E0100 — GlassWorm secondary range (R32) var sb strings.Builder sb.WriteString("x = \"") for i := 0; i < 12; i++ { sb.WriteRune(rune(0xE0100 + i)) } sb.WriteString("\"") source := sb.String() findings := ScanSource(source, "vs_supp.star") vsCount := 0 for _, f := range findings { if f.Category == "variation_selector" { vsCount++ } } if vsCount != 12 { t.Errorf("expected 12 variation_selector findings from supplement range, got %d", vsCount) } } // ── Verdict ───────────────────────────────── func TestVerdict_NoFindings(t *testing.T) { blocked, reason := Verdict(nil) if blocked { t.Error("expected not blocked for nil findings") } if reason != "" { t.Errorf("expected empty reason, got %q", reason) } } func TestVerdict_BidiAlwaysBlocks(t *testing.T) { findings := []Finding{{Rune: 0x202E, Category: "bidi_override"}} blocked, reason := Verdict(findings) if !blocked { t.Error("expected blocked for bidi_override") } if !strings.Contains(reason, "bidirectional override") { t.Errorf("expected bidi reason, got %q", reason) } } func TestVerdict_GlassWormBlocks(t *testing.T) { var findings []Finding for i := 0; i < 10; i++ { findings = append(findings, Finding{Rune: rune(0xFE00 + i), Category: "variation_selector"}) } blocked, reason := Verdict(findings) if !blocked { t.Error("expected blocked for 10 variation selectors") } if !strings.Contains(reason, "GlassWorm") { t.Errorf("expected GlassWorm reason, got %q", reason) } } func TestVerdict_GlassWormMixedVSAndTag(t *testing.T) { var findings []Finding for i := 0; i < 5; i++ { findings = append(findings, Finding{Rune: rune(0xFE00 + i), Category: "variation_selector"}) } for i := 0; i < 5; i++ { findings = append(findings, Finding{Rune: rune(0xE0001 + i), Category: "tag_character"}) } blocked, _ := Verdict(findings) if !blocked { t.Error("expected blocked for 5 VS + 5 tag = 10 total") } } func TestVerdict_FewVariationSelectorsPass(t *testing.T) { findings := []Finding{ {Rune: 0xFE0F, Category: "variation_selector"}, {Rune: 0xFE0E, Category: "variation_selector"}, } blocked, _ := Verdict(findings) if blocked { t.Error("expected not blocked for 2 variation selectors (emoji use)") } } func TestVerdict_ZeroWidthBelowThresholdPasses(t *testing.T) { var findings []Finding for i := 0; i < 5; i++ { findings = append(findings, Finding{Rune: 0x200B, Category: "zero_width"}) } blocked, _ := Verdict(findings) if blocked { t.Error("expected not blocked for 5 zero_width chars") } } func TestVerdict_ZeroWidthAboveThresholdBlocks(t *testing.T) { var findings []Finding for i := 0; i < 6; i++ { findings = append(findings, Finding{Rune: 0x200B, Category: "zero_width"}) } blocked, reason := Verdict(findings) if !blocked { t.Error("expected blocked for 6 zero_width chars") } if !strings.Contains(reason, "zero-width") { t.Errorf("expected zero-width reason, got %q", reason) } } func TestVerdict_OtherInvisibleDoesNotBlock(t *testing.T) { findings := []Finding{ {Rune: 0x3164, Category: "other_invisible"}, {Rune: 0xFFF9, Category: "other_invisible"}, } blocked, _ := Verdict(findings) if blocked { t.Error("expected not blocked for 2 other_invisible chars") } } // ── GlassWorm payload simulation ──────────── func TestGlassWormPayloadDetection(t *testing.T) { // Simulate a GlassWorm-style payload: encode each byte of a secret string // as a variation selector offset (U+FE00 + low nibble, U+E0100 + high nibble). // This is the core technique: invisible characters carrying data. secret := "eval(malicious_code)" var sb strings.Builder sb.WriteString("innocent_looking_var = \"") for _, b := range []byte(secret) { // Encode low nibble as VS1-VS16 (U+FE00-U+FE0F) sb.WriteRune(rune(0xFE00 + (int(b) & 0x0F))) // Encode high nibble as VS supplement (U+E0100-U+E010F) sb.WriteRune(rune(0xE0100 + ((int(b) >> 4) & 0x0F))) } sb.WriteString("\"") source := sb.String() findings := ScanSource(source, "glassworm_payload.star") // Should have 2 findings per character (low + high nibble) expectedFindings := len(secret) * 2 vsCount := 0 for _, f := range findings { if f.Category == "variation_selector" { vsCount++ } } if vsCount != expectedFindings { t.Errorf("expected %d variation_selector findings, got %d", expectedFindings, vsCount) } // Verdict should block blocked, reason := Verdict(findings) if !blocked { t.Errorf("GlassWorm payload should be blocked, got: %s", reason) } if !strings.Contains(reason, "GlassWorm") { t.Errorf("expected GlassWorm in reason, got: %s", reason) } } // ── Execution gate integration ────────────── func TestExecGateBlocksBidiSource(t *testing.T) { sb := New(DefaultConfig()) // Source with a bidi override character source := "x = \"\u202E\"" _, err := sb.Exec(context.Background(), "bidi.star", source, nil) if err == nil { t.Fatal("expected error from unicode gate for bidi override") } if !strings.Contains(err.Error(), "unicode security gate") { t.Errorf("expected 'unicode security gate' error, got: %v", err) } } func TestExecGateAllowsCleanSource(t *testing.T) { sb := New(DefaultConfig()) _, err := sb.Exec(context.Background(), "clean.star", "x = 42", nil) if err != nil { t.Fatalf("clean source should not be blocked: %v", err) } } func TestExecGateAllowsFewVS(t *testing.T) { sb := New(DefaultConfig()) source := "emoji = \"\u2764\uFE0F\"" _, err := sb.Exec(context.Background(), "emoji.star", source, nil) if err != nil { t.Fatalf("2 VS should not be blocked: %v", err) } }