Repository navigation
fix(archera): mask the API key after sanitizing error text #59
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
07dfef7
fix(archera): mask the API key after sanitizing error text
cristim 361e9c0
test(archera): add key-mask order cases and changelog entry
cristim bbfb901
fix(archera): sanitize context errors and withhold text with a hidden…
cristim fb2b6d2
fix(archera): check for a hidden key before capping error text
cristim 7fe7267
fix(archera): drop post-cap prefix check, test hidden key anywhere in…
cristim 1810d67
Merge origin/main into fix/56-archera-error-mask-after-sanitize
cristim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| package tools | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "net/http" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| const maskTestKey = "test-key-SYNTHETIC-0000" | ||
|
|
||
| // splitKey splices sep after every character of the key. | ||
| func splitKey(sep string) string { | ||
| return strings.Join(strings.Split(maskTestKey, ""), sep) | ||
| } | ||
|
|
||
| type failingTransport struct{ err error } | ||
|
|
||
| func (f failingTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, f.err } | ||
|
|
||
| // maskCases are echoes the strip-then-mask pipeline must fully redact. | ||
| func maskCases() map[string]string { | ||
| zw, bidi := "\u200b", "\u202e" | ||
| return map[string]string{ | ||
| "plain": "bad key " + maskTestKey, | ||
| "zero-width split": "bad key " + splitKey(zw), | ||
| "bidi split": "bad key " + splitKey(bidi), | ||
| "control split": "bad key " + splitKey("\x01"), | ||
| "newline split": "bad key " + splitKey("\n"), | ||
| "twice": "bad key " + maskTestKey + " and " + splitKey(zw), | ||
| "twice both split": splitKey(zw) + " then " + splitKey(bidi), | ||
| "straddles cap": strings.Repeat("a", 250) + maskTestKey, | ||
| "straddles split": strings.Repeat("a", 250) + splitKey(zw), | ||
| "last 10 bytes": strings.Repeat("a", 246) + splitKey(zw), | ||
| "invalid utf-8": "\xff" + maskTestKey + "\xfe", | ||
| } | ||
| } | ||
|
|
||
| // withholdCases are splitters the strip leaves in place: the key stays split | ||
| // (or displays as the key), so the whole text must be withheld. | ||
| func withholdCases() map[string]string { | ||
| seps := map[string]string{ | ||
| "combining acute": "\u0301", | ||
| "enclosing mark": "\u0488", | ||
| "variation selector": "\ufe0f", | ||
| "variation sel supp": "\U000e0100", | ||
| "cgj": "\u034f", | ||
| "hangul choseong": "\u115f", | ||
| "hangul jungseong": "\u1160", | ||
| "hangul filler": "\u3164", | ||
| "halfwidth hangul": "\uffa0", | ||
| "space": " ", | ||
| "no-break space": "\u00a0", | ||
| "ideographic space": "\u3000", | ||
| "invalid byte": "\xff", | ||
| "replacement char": "\ufffd", | ||
| "invalid and combine": "\xff\u0301", | ||
| } | ||
| out := map[string]string{} | ||
| for n, sep := range seps { | ||
| out[n] = "bad key " + splitKey(sep) | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| func TestArcheraErrorMasksKeyAfterSanitizing(t *testing.T) { | ||
| for name, msg := range maskCases() { | ||
| t.Run(name, func(t *testing.T) { | ||
| got := archeraError(errors.New(msg), maskTestKey).Error() | ||
| assert.NotContains(t, got, maskTestKey) | ||
| assert.LessOrEqual(t, len(got), 256) | ||
| assert.NotEqual(t, archeraWithheld, got, "a fully maskable echo is redacted, not withheld") | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestArcheraErrorWithholdsTextWhenKeyStaysReadable(t *testing.T) { | ||
| for name, msg := range withholdCases() { | ||
| t.Run(name, func(t *testing.T) { | ||
| got := archeraError(errors.New(msg), maskTestKey) | ||
| assert.EqualError(t, got, archeraWithheld) | ||
| assert.NotContains(t, got.Error(), maskTestKey) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestArcheraErrorWithholdsWholeTextWithKeyAnywhere(t *testing.T) { | ||
| fe0f, ff := splitKey("\ufe0f"), splitKey("\xff") | ||
| half := maskTestKey[:10] + "\ufe0f" + maskTestKey[10:] | ||
| cases := map[string]string{ | ||
| "text after": "bad key " + fe0f + " was rejected", | ||
| "key at start": fe0f + " was rejected", | ||
| "key in the middle": "org x: " + ff + " not valid for plan y", | ||
| "split at one place": "bad key " + half + " was rejected", | ||
| "two splitters": "bad key " + maskTestKey[:5] + "\ufe0f" + maskTestKey[5:12] + "\xff" + maskTestKey[12:] + " tail", | ||
| "split key then text": "bad key " + fe0f + strings.Repeat("z", 400), | ||
| } | ||
| for pad := 200; pad <= 262; pad++ { | ||
| cases[fmt.Sprintf("straddles cut pad %d", pad)] = strings.Repeat("a", pad) + fe0f + " was rejected" | ||
| } | ||
| for name, msg := range cases { | ||
| t.Run(name, func(t *testing.T) { | ||
| assert.EqualError(t, archeraError(errors.New(msg), maskTestKey), archeraWithheld) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestArcheraErrorTrimsKeyBeforeMasking(t *testing.T) { | ||
| got := archeraError(errors.New("bad key "+splitKey("\u200b")), " "+maskTestKey+"\n").Error() | ||
| assert.Equal(t, "bad key [redacted]", got) | ||
| } | ||
|
|
||
| func TestArcheraErrorLeavesNoKeyPrefixAtCap(t *testing.T) { | ||
| for _, echo := range []string{maskTestKey, splitKey("\u200b")} { | ||
| got := archeraError(errors.New(strings.Repeat("a", 244)+echo), maskTestKey).Error() | ||
| assert.NotContains(t, got, maskTestKey[:8]) | ||
| } | ||
| } | ||
|
|
||
| func TestArcheraErrorContextErrorsAreSanitizedAndKeepIdentity(t *testing.T) { | ||
| assert.Same(t, context.Canceled, archeraError(context.Canceled, maskTestKey)) | ||
| for name, echo := range map[string]string{ | ||
| "plain": maskTestKey, "split": splitKey("\u200b"), "unstripped split": splitKey("\u0301"), | ||
| } { | ||
| for _, base := range []error{context.DeadlineExceeded, context.Canceled} { | ||
| t.Run(name, func(t *testing.T) { | ||
| got := archeraError(fmt.Errorf("dial %s: %w", echo, base), maskTestKey) | ||
| assert.NotContains(t, got.Error(), maskTestKey) | ||
| assert.ErrorIs(t, got, base) | ||
| }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestArcheraCleanStringCapBoundary(t *testing.T) { | ||
| assert.Len(t, archeraCleanString(strings.Repeat("a", 256)), 256) | ||
| assert.Len(t, archeraCleanString(strings.Repeat("a", 257)), 256) | ||
| } | ||
|
|
||
| func TestArcheraErrorMasksKeyOnRealPaths(t *testing.T) { | ||
| check := func(t *testing.T, rt http.RoundTripper) { | ||
| t.Helper() | ||
| res, protocol, err := callArchera(t, rt, nil) | ||
| require.NoError(t, err) | ||
| require.True(t, res.IsError) | ||
| assert.NotContains(t, resultText(res), maskTestKey) | ||
| assert.NotContains(t, protocol, maskTestKey) | ||
| } | ||
| all := maskCases() | ||
| for n, v := range withholdCases() { | ||
| all["withhold "+n] = v | ||
| } | ||
| for name, echo := range all { | ||
| t.Run("decode "+name, func(t *testing.T) { | ||
| setArcheraEnv(t) | ||
| t.Setenv(EnvArcheraAPIKey, maskTestKey) | ||
| var w map[string]any | ||
| require.NoError(t, json.Unmarshal(readWire(t), &w)) | ||
| entryOf(w)["contract_term"] = echo | ||
| body, err := json.Marshal(w) | ||
| require.NoError(t, err) | ||
| check(t, okTransport(body)) | ||
| }) | ||
| t.Run("transport "+name, func(t *testing.T) { | ||
| setArcheraEnv(t) | ||
| t.Setenv(EnvArcheraAPIKey, maskTestKey) | ||
| check(t, failingTransport{errors.New(echo)}) | ||
| }) | ||
| t.Run("transport deadline "+name, func(t *testing.T) { | ||
| setArcheraEnv(t) | ||
| t.Setenv(EnvArcheraAPIKey, maskTestKey) | ||
| check(t, failingTransport{fmt.Errorf("%s: %w", echo, context.DeadlineExceeded)}) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestArcheraErrorLeavesNoKeyPrefixAtAnyCut(t *testing.T) { | ||
| for name, sep := range map[string]string{"variation selector": "\ufe0f", "invalid byte": "\xff"} { | ||
| for pad := 220; pad <= 262; pad++ { | ||
| got := archeraError(errors.New(strings.Repeat("a", pad)+splitKey(sep)), maskTestKey).Error() | ||
| for k := 6; k <= len(maskTestKey); k++ { | ||
| require.NotContains(t, archeraSkeleton(got), maskTestKey[:k], "%s pad %d", name, pad) | ||
| } | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Remove spacing combining marks from the key skeleton.
If a vendor inserts U+0903 (an
Mcmark) between the key’s characters,archeraStripUnsafeleaves it in the text. This check also leaves it in place, so the readable key echo reaches the returned error. Includeunicode.Mcin the skeleton filter and test a split key with that mark.Proposed change
📝 Committable suggestion
🤖 Prompt for AI Agents