From f94542dde3d7c59507e4ca484e875055ce4b2fcd Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Tue, 29 Sep 2026 17:20:58 -0400 Subject: [PATCH 1/2] Reject line breaks in Akamai and Pipedream config values The Akamai (.edgerc) and Pipedream config files are built by joining `key = value` lines, so a line break in an item field would start a new key or section in the generated file. Trim surrounding whitespace from each value, then return an error if a line break remains. These files are read by INI parsers without escape syntax, so quoting or escaping (as done for MySQL in #672) would not be read back correctly, and valid credentials never contain line breaks. The parsers already ignore surrounding whitespace, so trimming keeps a stray leading or trailing newline, such as from a paste, working as before. The error names the field but never includes its value. --- plugins/akamai/api_client_credentials.go | 39 +++++++----- plugins/akamai/api_client_credentials_test.go | 63 +++++++++++++++++++ plugins/pipedream/api_key.go | 30 ++++++--- plugins/pipedream/api_key_test.go | 59 +++++++++++++++++ 4 files changed, 170 insertions(+), 21 deletions(-) diff --git a/plugins/akamai/api_client_credentials.go b/plugins/akamai/api_client_credentials.go index 87fda0dbf..b11ecbc15 100644 --- a/plugins/akamai/api_client_credentials.go +++ b/plugins/akamai/api_client_credentials.go @@ -2,6 +2,8 @@ package akamai import ( "context" + "fmt" + "strings" "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/importer" @@ -85,25 +87,34 @@ func APIClientCredentials() schema.CredentialType { } func configFile(in sdk.ProvisionInput) ([]byte, error) { - contents := "[default]\n" - - if clientsecret, ok := in.ItemFields[fieldname.ClientSecret]; ok { - contents += "client_secret = " + clientsecret + "\n" - } - - if host, ok := in.ItemFields[fieldname.Host]; ok { - contents += "host = " + host + "\n" + fields := []struct { + name sdk.FieldName + key string + }{ + {fieldname.ClientSecret, "client_secret"}, + {fieldname.Host, "host"}, + {fieldname.AccessToken, "access_token"}, + {fieldname.ClientToken, "client_token"}, } - if accesstoken, ok := in.ItemFields[fieldname.AccessToken]; ok { - contents += "access_token = " + accesstoken + "\n" - } + var contents strings.Builder + contents.WriteString("[default]\n") - if clienttoken, ok := in.ItemFields[fieldname.ClientToken]; ok { - contents += "client_token = " + clienttoken + "\n" + for _, field := range fields { + value, ok := in.ItemFields[field.name] + if !ok { + continue + } + // INI parsers ignore surrounding whitespace, so trimming it only drops + // harmless leading or trailing line breaks, such as from a paste. + value = strings.TrimSpace(value) + if strings.ContainsAny(value, "\r\n") { + return nil, fmt.Errorf("Akamai credential field %q cannot contain line breaks", field.name) + } + contents.WriteString(field.key + " = " + value + "\n") } - return []byte(contents), nil + return []byte(contents.String()), nil } // Load credentials from the ~/.edgerc file. diff --git a/plugins/akamai/api_client_credentials_test.go b/plugins/akamai/api_client_credentials_test.go index 3ec4bc893..9626528fc 100644 --- a/plugins/akamai/api_client_credentials_test.go +++ b/plugins/akamai/api_client_credentials_test.go @@ -1,11 +1,14 @@ package akamai import ( + "fmt" "testing" "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/plugintest" "github.com/1Password/shell-plugins/sdk/schema/fieldname" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAPIClientCredentialsProvisioner(t *testing.T) { @@ -30,6 +33,66 @@ func TestAPIClientCredentialsProvisioner(t *testing.T) { }) } +func TestConfigFileRejectsLineBreaks(t *testing.T) { + validFields := map[sdk.FieldName]string{ + fieldname.ClientSecret: "abcdE23FNkBxy456z25qx9Yp5CPUxlEfQeTDkfh4QA=I", + fieldname.Host: "akab-lmn789n2k53w7qrs-nfkxaa4lfk3kd6ym.luna.akamaiapis.net", + fieldname.AccessToken: "akab-zyx987xa6osbli4k-e7jf5ikib5jknes3", + fieldname.ClientToken: "akab-nomoflavjuc4422e-fa2xznerxrm3teg7", + } + + for field, validValue := range validFields { + for name, lineBreak := range map[string]string{ + "line feed": "\n", + "carriage return": "\r", + } { + t.Run(fmt.Sprintf("%s/%s", field, name), func(t *testing.T) { + fields := make(map[sdk.FieldName]string, len(validFields)) + for key, value := range validFields { + fields[key] = value + } + fields[field] = validValue + lineBreak + "debug = true" + + contents, err := configFile(sdk.ProvisionInput{ItemFields: fields}) + + assert.Nil(t, contents) + require.EqualError(t, err, fmt.Sprintf("Akamai credential field %q cannot contain line breaks", field)) + assert.NotContains(t, err.Error(), "debug = true") + }) + } + } +} + +func TestConfigFileTrimsSurroundingWhitespace(t *testing.T) { + validFields := map[sdk.FieldName]string{ + fieldname.ClientSecret: "abcdE23FNkBxy456z25qx9Yp5CPUxlEfQeTDkfh4QA=I", + fieldname.Host: "akab-lmn789n2k53w7qrs-nfkxaa4lfk3kd6ym.luna.akamaiapis.net", + fieldname.AccessToken: "akab-zyx987xa6osbli4k-e7jf5ikib5jknes3", + fieldname.ClientToken: "akab-nomoflavjuc4422e-fa2xznerxrm3teg7", + } + expected, err := configFile(sdk.ProvisionInput{ItemFields: validFields}) + require.NoError(t, err) + + for name, pad := range map[string]func(string) string{ + "trailing line feed": func(v string) string { return v + "\n" }, + "trailing CRLF": func(v string) string { return v + "\r\n" }, + "leading line feed": func(v string) string { return "\n" + v }, + "surrounding spaces, tabs": func(v string) string { return " \t" + v + "\t " }, + } { + t.Run(name, func(t *testing.T) { + fields := make(map[sdk.FieldName]string, len(validFields)) + for key, value := range validFields { + fields[key] = pad(value) + } + + contents, err := configFile(sdk.ProvisionInput{ItemFields: fields}) + + require.NoError(t, err) + assert.Equal(t, string(expected), string(contents)) + }) + } +} + func TestAPIClientCredentialsImporter(t *testing.T) { plugintest.TestImporter(t, APIClientCredentials().Importer, map[string]plugintest.ImportCase{ "config file with single credential": { diff --git a/plugins/pipedream/api_key.go b/plugins/pipedream/api_key.go index 7a1a23dad..2cd76468e 100644 --- a/plugins/pipedream/api_key.go +++ b/plugins/pipedream/api_key.go @@ -2,6 +2,8 @@ package pipedream import ( "context" + "fmt" + "strings" "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/importer" @@ -88,15 +90,29 @@ type Config struct { } func pipedreamConfig(in sdk.ProvisionInput) ([]byte, error) { - contents := "" - - if apikey, ok := in.ItemFields[fieldname.APIKey]; ok { - contents += "api_key = " + apikey + "\n" + fields := []struct { + name sdk.FieldName + key string + }{ + {fieldname.APIKey, "api_key"}, + {fieldname.OrgID, "org_id"}, } - if orgid, ok := in.ItemFields[fieldname.OrgID]; ok { - contents += "org_id = " + orgid + "\n" + var contents strings.Builder + + for _, field := range fields { + value, ok := in.ItemFields[field.name] + if !ok { + continue + } + // INI parsers ignore surrounding whitespace, so trimming it only drops + // harmless leading or trailing line breaks, such as from a paste. + value = strings.TrimSpace(value) + if strings.ContainsAny(value, "\r\n") { + return nil, fmt.Errorf("Pipedream credential field %q cannot contain line breaks", field.name) + } + contents.WriteString(field.key + " = " + value + "\n") } - return []byte(contents), nil + return []byte(contents.String()), nil } diff --git a/plugins/pipedream/api_key_test.go b/plugins/pipedream/api_key_test.go index 3104c3a9a..1ba47c724 100644 --- a/plugins/pipedream/api_key_test.go +++ b/plugins/pipedream/api_key_test.go @@ -1,11 +1,14 @@ package pipedream import ( + "fmt" "testing" "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/plugintest" "github.com/1Password/shell-plugins/sdk/schema/fieldname" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAPIKeyProvisioner(t *testing.T) { @@ -26,6 +29,62 @@ func TestAPIKeyProvisioner(t *testing.T) { }) } +func TestPipedreamConfigRejectsLineBreaks(t *testing.T) { + validFields := map[sdk.FieldName]string{ + fieldname.APIKey: "ugvfxesz62ycsl42z49c0t1hjexample", + fieldname.OrgID: "YbEXAMPLE", + } + + for field, validValue := range validFields { + for name, lineBreak := range map[string]string{ + "line feed": "\n", + "carriage return": "\r", + } { + t.Run(fmt.Sprintf("%s/%s", field, name), func(t *testing.T) { + fields := make(map[sdk.FieldName]string, len(validFields)) + for key, value := range validFields { + fields[key] = value + } + fields[field] = validValue + lineBreak + "[other]" + + contents, err := pipedreamConfig(sdk.ProvisionInput{ItemFields: fields}) + + assert.Nil(t, contents) + require.EqualError(t, err, fmt.Sprintf("Pipedream credential field %q cannot contain line breaks", field)) + assert.NotContains(t, err.Error(), "[other]") + }) + } + } +} + +func TestPipedreamConfigTrimsSurroundingWhitespace(t *testing.T) { + validFields := map[sdk.FieldName]string{ + fieldname.APIKey: "ugvfxesz62ycsl42z49c0t1hjexample", + fieldname.OrgID: "YbEXAMPLE", + } + expected, err := pipedreamConfig(sdk.ProvisionInput{ItemFields: validFields}) + require.NoError(t, err) + + for name, pad := range map[string]func(string) string{ + "trailing line feed": func(v string) string { return v + "\n" }, + "trailing CRLF": func(v string) string { return v + "\r\n" }, + "leading line feed": func(v string) string { return "\n" + v }, + "surrounding spaces, tabs": func(v string) string { return " \t" + v + "\t " }, + } { + t.Run(name, func(t *testing.T) { + fields := make(map[sdk.FieldName]string, len(validFields)) + for key, value := range validFields { + fields[key] = pad(value) + } + + contents, err := pipedreamConfig(sdk.ProvisionInput{ItemFields: fields}) + + require.NoError(t, err) + assert.Equal(t, string(expected), string(contents)) + }) + } +} + func TestAPIKeyImporter(t *testing.T) { plugintest.TestImporter(t, APIKey().Importer, map[string]plugintest.ImportCase{ "config file": { From 201d6986ba797749e192149c658b53f42b464f4c Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Tue, 29 Sep 2026 17:52:26 -0400 Subject: [PATCH 2/2] fix(ci): Lowercase line-break error strings for staticcheck ST1005 --- plugins/akamai/api_client_credentials.go | 2 +- plugins/akamai/api_client_credentials_test.go | 2 +- plugins/pipedream/api_key.go | 2 +- plugins/pipedream/api_key_test.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/akamai/api_client_credentials.go b/plugins/akamai/api_client_credentials.go index b11ecbc15..847fb472a 100644 --- a/plugins/akamai/api_client_credentials.go +++ b/plugins/akamai/api_client_credentials.go @@ -109,7 +109,7 @@ func configFile(in sdk.ProvisionInput) ([]byte, error) { // harmless leading or trailing line breaks, such as from a paste. value = strings.TrimSpace(value) if strings.ContainsAny(value, "\r\n") { - return nil, fmt.Errorf("Akamai credential field %q cannot contain line breaks", field.name) + return nil, fmt.Errorf("line breaks are not allowed in the Akamai %q field", field.name) } contents.WriteString(field.key + " = " + value + "\n") } diff --git a/plugins/akamai/api_client_credentials_test.go b/plugins/akamai/api_client_credentials_test.go index 9626528fc..c6ab0be7e 100644 --- a/plugins/akamai/api_client_credentials_test.go +++ b/plugins/akamai/api_client_credentials_test.go @@ -56,7 +56,7 @@ func TestConfigFileRejectsLineBreaks(t *testing.T) { contents, err := configFile(sdk.ProvisionInput{ItemFields: fields}) assert.Nil(t, contents) - require.EqualError(t, err, fmt.Sprintf("Akamai credential field %q cannot contain line breaks", field)) + require.EqualError(t, err, fmt.Sprintf("line breaks are not allowed in the Akamai %q field", field)) assert.NotContains(t, err.Error(), "debug = true") }) } diff --git a/plugins/pipedream/api_key.go b/plugins/pipedream/api_key.go index 2cd76468e..6cf2b2252 100644 --- a/plugins/pipedream/api_key.go +++ b/plugins/pipedream/api_key.go @@ -109,7 +109,7 @@ func pipedreamConfig(in sdk.ProvisionInput) ([]byte, error) { // harmless leading or trailing line breaks, such as from a paste. value = strings.TrimSpace(value) if strings.ContainsAny(value, "\r\n") { - return nil, fmt.Errorf("Pipedream credential field %q cannot contain line breaks", field.name) + return nil, fmt.Errorf("line breaks are not allowed in the Pipedream %q field", field.name) } contents.WriteString(field.key + " = " + value + "\n") } diff --git a/plugins/pipedream/api_key_test.go b/plugins/pipedream/api_key_test.go index 1ba47c724..fb94b1928 100644 --- a/plugins/pipedream/api_key_test.go +++ b/plugins/pipedream/api_key_test.go @@ -50,7 +50,7 @@ func TestPipedreamConfigRejectsLineBreaks(t *testing.T) { contents, err := pipedreamConfig(sdk.ProvisionInput{ItemFields: fields}) assert.Nil(t, contents) - require.EqualError(t, err, fmt.Sprintf("Pipedream credential field %q cannot contain line breaks", field)) + require.EqualError(t, err, fmt.Sprintf("line breaks are not allowed in the Pipedream %q field", field)) assert.NotContains(t, err.Error(), "[other]") }) }