100 lines
2.1 KiB
Go
100 lines
2.1 KiB
Go
package security
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"regexp"
|
||
|
|
"strings"
|
||
|
|
"unicode"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
// MaxCampaignPromptRunes caps custom campaign / brand AI prompts (prompt-injection surface).
|
||
|
|
MaxCampaignPromptRunes = 8000
|
||
|
|
// MaxBrandFieldRunes caps individual brand kit text fields.
|
||
|
|
MaxBrandFieldRunes = 2000
|
||
|
|
// MaxBrandListItems caps dos/donts/preferred_terms entries.
|
||
|
|
MaxBrandListItems = 40
|
||
|
|
// MaxBrandListItemRunes caps each list entry.
|
||
|
|
MaxBrandListItemRunes = 200
|
||
|
|
)
|
||
|
|
|
||
|
|
var injectPhrase = regexp.MustCompile(`(?i)(ignore\s+previous|system\s*:|assistant\s*:|<\s*/?\s*script)`)
|
||
|
|
|
||
|
|
// SanitizePrompt strips controls, soft-filters injection phrases, and truncates by runes.
|
||
|
|
func SanitizePrompt(s string, maxRunes int) string {
|
||
|
|
if maxRunes <= 0 {
|
||
|
|
maxRunes = MaxCampaignPromptRunes
|
||
|
|
}
|
||
|
|
s = stripControls(strings.TrimSpace(s))
|
||
|
|
s = injectPhrase.ReplaceAllString(s, "[filtered]")
|
||
|
|
return TruncateRunes(s, maxRunes)
|
||
|
|
}
|
||
|
|
|
||
|
|
// CapPromptLength reports whether s exceeds max (rune count).
|
||
|
|
func CapPromptLength(s string, maxRunes int) bool {
|
||
|
|
if maxRunes <= 0 {
|
||
|
|
maxRunes = MaxCampaignPromptRunes
|
||
|
|
}
|
||
|
|
n := 0
|
||
|
|
for range s {
|
||
|
|
n++
|
||
|
|
if n > maxRunes {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// SanitizeBrandList cleans and bounds a brand kit string list.
|
||
|
|
func SanitizeBrandList(in []string) []string {
|
||
|
|
if len(in) == 0 {
|
||
|
|
return []string{}
|
||
|
|
}
|
||
|
|
out := make([]string, 0, len(in))
|
||
|
|
seen := map[string]struct{}{}
|
||
|
|
for _, s := range in {
|
||
|
|
s = SanitizePrompt(s, MaxBrandListItemRunes)
|
||
|
|
if s == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
key := strings.ToLower(s)
|
||
|
|
if _, ok := seen[key]; ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
seen[key] = struct{}{}
|
||
|
|
out = append(out, s)
|
||
|
|
if len(out) >= MaxBrandListItems {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func stripControls(s string) string {
|
||
|
|
if s == "" {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
var b strings.Builder
|
||
|
|
b.Grow(len(s))
|
||
|
|
for _, r := range s {
|
||
|
|
if r == '\n' || r == '\t' || unicode.IsPrint(r) {
|
||
|
|
b.WriteRune(r)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return b.String()
|
||
|
|
}
|
||
|
|
|
||
|
|
// TruncateRunes truncates s to at most max runes.
|
||
|
|
func TruncateRunes(s string, max int) string {
|
||
|
|
if max <= 0 {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
n := 0
|
||
|
|
for i := range s {
|
||
|
|
if n == max {
|
||
|
|
return s[:i]
|
||
|
|
}
|
||
|
|
n++
|
||
|
|
}
|
||
|
|
return s
|
||
|
|
}
|