-
Notifications
You must be signed in to change notification settings - Fork 86
feat: sanitize input for hidden control characters and whitespace (#271) #313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e7c5406
dc2f5f9
fbd8612
8bf08d1
f239972
284f8c2
556debf
d9ac78f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,11 +3,45 @@ package routing | |
| import ( | ||
| "strconv" | ||
| "strings" | ||
| "unicode" | ||
|
|
||
| "github.com/Boxkit-Labs/stellar-address-kit/packages/core-go/address" | ||
| "github.com/Boxkit-Labs/stellar-address-kit/packages/core-go/muxed" | ||
| ) | ||
|
|
||
| func isHiddenOrWhitespace(r rune) bool { | ||
| if unicode.IsSpace(r) || unicode.IsControl(r) { | ||
| return true | ||
| } | ||
| switch { | ||
| case r == 0xFEFF, r == 0x00AD: | ||
| return true | ||
| case r >= 0x200B && r <= 0x200F: | ||
| return true | ||
| case r >= 0x2028 && r <= 0x202F: | ||
| return true | ||
| case r >= 0x2060 && r <= 0x206F: | ||
| return true | ||
| case r >= 0xFFF9 && r <= 0xFFFB: | ||
|
Comment on lines
+17
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,140p' packages/core-go/routing/extract.go
printf '\n-- references --\n'
rg -n "isHiddenOrWhitespace|sanitizeDestination|address\\.Parse" packages/core-goRepository: Boxkit-Labs/stellar-address-kit Length of output: 4340 🏁 Script executed: printf '%s\n' '-- address package map --'
ast-grep outline packages/core-go/address
printf '%s\n' '-- Parse definitions and validation --'
rg -n -A70 -B15 'func Parse|func Parse[A-Za-z]*|invalid|base32|checksum|alphabet|rune' packages/core-go/address
printf '%s\n' '-- routing tests around sanitization --'
rg -n -A35 -B15 'SanitizedHiddenChars|sanitize|hidden|whitespace|202[4-9]|202[ABCDEF]' packages/core-go --glob '*_test.go'Repository: Boxkit-Labs/stellar-address-kit Length of output: 50387 🏁 Script executed: goroot="$(go env GOROOT 2>/dev/null || true)"
if [ -z "$goroot" ] || [ ! -f "$goroot/src/unicode/tables.go" ]; then
printf '%s\n' 'Go standard-library source unavailable'
exit 0
fi
rg -n -A20 -B8 'func IsSpace|White_Space|0x2028|0x2029|0x202F' "$goroot/src/unicode/tables.go" "$goroot/src/unicode/digit.go" "$goroot/src/unicode/graphic.go" 2>/dev/nullRepository: Boxkit-Labs/stellar-address-kit Length of output: 19130 Do not strip visible punctuation.
🤖 Prompt for AI Agents |
||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func sanitizeDestination(dest string) (string, bool) { | ||
| if !strings.ContainsFunc(dest, isHiddenOrWhitespace) { | ||
| return dest, false | ||
| } | ||
| var sb strings.Builder | ||
| sb.Grow(len(dest)) | ||
| for _, r := range dest { | ||
| if !isHiddenOrWhitespace(r) { | ||
| sb.WriteRune(r) | ||
| } | ||
| } | ||
| return sb.String(), true | ||
| } | ||
|
|
||
| // normalizeUnsupportedMemoType canonicalizes a memo type string by lower-casing it | ||
| // and stripping underscores and hyphens, then maps it to a known unsupported type. | ||
| // Uses strings.Builder to avoid intermediate string allocations from chained ReplaceAll/ToLower. | ||
|
|
@@ -59,11 +93,29 @@ func ExtractRouting(input RoutingInput) RoutingResult { | |
| } | ||
| } | ||
|
|
||
| parsed, err := address.Parse(input.Destination) | ||
| sanitizedDest, wasSanitized := sanitizeDestination(input.Destination) | ||
|
|
||
| initWarnings := func(additional ...address.Warning) []address.Warning { | ||
| capSize := len(additional) | ||
| if wasSanitized { | ||
| capSize++ | ||
| } | ||
| w := make([]address.Warning, 0, capSize) | ||
| if wasSanitized { | ||
| w = append(w, address.Warning{ | ||
| Code: address.WarnSanitizedHiddenChars, | ||
| Severity: "info", | ||
| Message: "Destination address contained non-printable characters or whitespace that were stripped.", | ||
| }) | ||
| } | ||
| return append(w, additional...) | ||
| } | ||
|
|
||
| parsed, err := address.Parse(sanitizedDest) | ||
| if err != nil { | ||
| return RoutingResult{ | ||
| RoutingSource: "none", | ||
| Warnings: []address.Warning{}, | ||
| Warnings: initWarnings(), | ||
| DestinationError: &DestinationError{ | ||
| Code: address.ErrUnknownPrefix, | ||
| Message: err.Error(), | ||
|
|
@@ -72,16 +124,18 @@ func ExtractRouting(input RoutingInput) RoutingResult { | |
| } | ||
|
|
||
| if parsed.Kind == address.KindC { | ||
| warnings := initWarnings() | ||
| warnings = append(warnings, address.Warning{ | ||
| Code: address.WarnInvalidDestination, | ||
| Severity: "error", | ||
| Message: "C address is not a valid destination", | ||
| Context: &address.WarningContext{ | ||
| DestinationKind: "C", | ||
| }, | ||
| }) | ||
| return RoutingResult{ | ||
| RoutingSource: "none", | ||
| Warnings: []address.Warning{{ | ||
| Code: address.WarnInvalidDestination, | ||
| Severity: "error", | ||
| Message: "C address is not a valid destination", | ||
| Context: &address.WarningContext{ | ||
| DestinationKind: "C", | ||
| }, | ||
| }}, | ||
| Warnings: warnings, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -90,17 +144,15 @@ func ExtractRouting(input RoutingInput) RoutingResult { | |
| if err != nil { | ||
| return RoutingResult{ | ||
| RoutingSource: "none", | ||
| Warnings: []address.Warning{}, | ||
| Warnings: initWarnings(), | ||
| DestinationError: &DestinationError{ | ||
| Code: address.ErrUnknownPrefix, | ||
| Message: err.Error(), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // Pre-allocate with capacity for existing warnings plus at most one more. | ||
| warnings := make([]address.Warning, 0, len(parsed.Warnings)+1) | ||
| warnings = append(warnings, parsed.Warnings...) | ||
| warnings := initWarnings(parsed.Warnings...) | ||
| memoValue := stringValue(input.MemoValue) | ||
|
|
||
| // isAllDigits replaces the regex match to avoid heap allocation. | ||
|
|
@@ -128,9 +180,7 @@ func ExtractRouting(input RoutingInput) RoutingResult { | |
|
|
||
| var routingID *RoutingID | ||
| routingSource := "none" | ||
| // Pre-allocate with capacity for existing address warnings plus at most two memo warnings. | ||
| warnings := make([]address.Warning, 0, len(parsed.Warnings)+2) | ||
| warnings = append(warnings, parsed.Warnings...) | ||
| warnings := initWarnings(parsed.Warnings...) | ||
| memoValue := stringValue(input.MemoValue) | ||
|
|
||
| if input.MemoType == "id" { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Cover all Unicode format characters.
Line 25 omits invisible
Cfcharacters such as U+061C and U+180E. A destination containing either character is not sanitized, so routing fails instead of returning the sanitized account andSANITIZED_HIDDEN_CHARSwarning. Replace the partial range list with complete control and format classification. Add regression cases for omittedCfcharacters.🤖 Prompt for AI Agents