Skip to content

Commit 793c49f

Browse files
committed
fix(security): restrict release workflow and block env overrides
- gate release workflow to same-repo PRs before fast-forwarding\n- apply SUPABASE_* overrides only to user-configurable config fields\n- recurse through embedded base config while skipping internal and map-backed fields\n- sync generated API client files to match current infrastructure spec\n- stabilize logout tests by using the Viper YES flag instead of stdin timing
1 parent bce7051 commit 793c49f

6 files changed

Lines changed: 229 additions & 11 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ permissions:
1111
jobs:
1212
fast-forward:
1313
if: |
14+
github.event.pull_request.head.repo.full_name == github.repository &&
1415
github.event.pull_request.head.ref == 'develop' &&
1516
github.event.pull_request.base.ref == 'main' &&
1617
github.event.review.state == 'approved'

internal/logout/logout_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66
"testing"
77

88
"github.com/spf13/afero"
9+
"github.com/spf13/viper"
910
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
1112
"github.com/supabase/cli/internal/testing/apitest"
12-
"github.com/supabase/cli/internal/testing/fstest"
1313
"github.com/supabase/cli/internal/utils"
1414
"github.com/supabase/cli/internal/utils/credentials"
1515
"github.com/zalando/go-keyring"
@@ -20,7 +20,8 @@ func TestLogoutCommand(t *testing.T) {
2020

2121
t.Run("login with token and logout", func(t *testing.T) {
2222
keyring.MockInitWithError(keyring.ErrUnsupportedPlatform)
23-
t.Cleanup(fstest.MockStdin(t, "y"))
23+
viper.Set("YES", true)
24+
t.Cleanup(viper.Reset)
2425
// Setup in-memory fs
2526
fsys := afero.NewMemMapFs()
2627
require.NoError(t, utils.SaveAccessToken(token, fsys))
@@ -35,10 +36,11 @@ func TestLogoutCommand(t *testing.T) {
3536

3637
t.Run("removes all Supabase CLI credentials", func(t *testing.T) {
3738
keyring.MockInit()
39+
viper.Set("YES", true)
40+
t.Cleanup(viper.Reset)
3841
require.NoError(t, credentials.StoreProvider.Set(utils.CurrentProfile.Name, token))
3942
require.NoError(t, credentials.StoreProvider.Set("project1", "password1"))
4043
require.NoError(t, credentials.StoreProvider.Set("project2", "password2"))
41-
t.Cleanup(fstest.MockStdin(t, "y"))
4244
// Run test
4345
err := Run(context.Background(), os.Stdout, afero.NewMemMapFs())
4446
// Check error
@@ -70,7 +72,8 @@ func TestLogoutCommand(t *testing.T) {
7072

7173
t.Run("exits 0 if not logged in", func(t *testing.T) {
7274
keyring.MockInit()
73-
t.Cleanup(fstest.MockStdin(t, "y"))
75+
viper.Set("YES", true)
76+
t.Cleanup(viper.Reset)
7477
// Setup in-memory fs
7578
fsys := afero.NewMemMapFs()
7679
// Run test
@@ -81,7 +84,8 @@ func TestLogoutCommand(t *testing.T) {
8184

8285
t.Run("throws error on failure to delete", func(t *testing.T) {
8386
keyring.MockInitWithError(keyring.ErrNotFound)
84-
t.Cleanup(fstest.MockStdin(t, "y"))
87+
viper.Set("YES", true)
88+
t.Cleanup(viper.Reset)
8589
// Setup empty home directory
8690
t.Setenv("HOME", "")
8791
// Setup in-memory fs

pkg/api/client.gen.go

Lines changed: 131 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/api/types.gen.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/config/config.go

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
_ "embed"
7+
"encoding"
78
"encoding/base64"
89
"encoding/json"
910
"fmt"
@@ -16,13 +17,15 @@ import (
1617
"os"
1718
"path"
1819
"path/filepath"
20+
"reflect"
1921
"regexp"
2022
"slices"
2123
"sort"
2224
"strconv"
2325
"strings"
2426
"text/template"
2527
"time"
28+
"unicode"
2629

2730
"github.com/BurntSushi/toml"
2831
"github.com/docker/go-units"
@@ -458,17 +461,15 @@ func (c *config) Eject(w io.Writer) error {
458461

459462
// Loads custom config file to struct fields tagged with toml.
460463
func (c *config) loadFromFile(filename string, fsys fs.FS) error {
461-
v := viper.NewWithOptions(
462-
viper.ExperimentalBindStruct(),
463-
viper.EnvKeyReplacer(strings.NewReplacer(".", "_")),
464-
)
465-
v.SetEnvPrefix("SUPABASE")
466-
v.AutomaticEnv()
464+
v := viper.New()
467465
if err := c.mergeDefaultValues(v); err != nil {
468466
return err
469467
} else if err := mergeFileConfig(v, filename, fsys); err != nil {
470468
return err
471469
}
470+
if err := bindUserConfigEnv(v, reflect.TypeOf(*c), ""); err != nil {
471+
return err
472+
}
472473
// Find [remotes.*] block to override base config
473474
idToName := map[string]string{}
474475
for name, remote := range v.GetStringMap("remotes") {
@@ -488,6 +489,66 @@ func (c *config) loadFromFile(filename string, fsys fs.FS) error {
488489
return c.load(v)
489490
}
490491

492+
func bindUserConfigEnv(v *viper.Viper, t reflect.Type, prefix string) error {
493+
textUnmarshaler := reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
494+
for i := 0; i < t.NumField(); i++ {
495+
field := t.Field(i)
496+
if field.Anonymous {
497+
if err := bindUserConfigEnv(v, field.Type, prefix); err != nil {
498+
return err
499+
}
500+
continue
501+
}
502+
if field.PkgPath != "" {
503+
continue
504+
}
505+
if tag := strings.Split(field.Tag.Get("toml"), ",")[0]; tag == "-" {
506+
continue
507+
}
508+
key := strings.Split(field.Tag.Get("json"), ",")[0]
509+
if key == "-" {
510+
continue
511+
} else if key == "" {
512+
key = toSnakeCase(field.Name)
513+
}
514+
if len(prefix) > 0 {
515+
key = prefix + "." + key
516+
}
517+
fieldType := field.Type
518+
if fieldType.Kind() == reflect.Ptr {
519+
fieldType = fieldType.Elem()
520+
}
521+
if fieldType.Kind() == reflect.Map {
522+
continue
523+
}
524+
if fieldType.Kind() == reflect.Struct && !fieldType.Implements(textUnmarshaler) && !reflect.PointerTo(fieldType).Implements(textUnmarshaler) {
525+
if err := bindUserConfigEnv(v, fieldType, key); err != nil {
526+
return err
527+
}
528+
continue
529+
}
530+
envKey := "SUPABASE_" + strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
531+
if value, ok := os.LookupEnv(envKey); ok {
532+
v.Set(key, value)
533+
}
534+
}
535+
return nil
536+
}
537+
538+
func toSnakeCase(s string) string {
539+
var b strings.Builder
540+
for i, r := range s {
541+
if unicode.IsUpper(r) {
542+
if i > 0 {
543+
b.WriteByte('_')
544+
}
545+
r = unicode.ToLower(r)
546+
}
547+
b.WriteRune(r)
548+
}
549+
return b.String()
550+
}
551+
491552
func (c *config) mergeDefaultValues(v *viper.Viper) error {
492553
v.SetConfigType("toml")
493554
var buf bytes.Buffer

pkg/config/config_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,21 @@ func TestRemoteOverride(t *testing.T) {
154154
})
155155
}
156156

157+
func TestEnvOverridesSkipInternalFields(t *testing.T) {
158+
config := NewConfig()
159+
fsys := fs.MapFS{
160+
"supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed},
161+
"supabase/templates/invite.html": &fs.MapFile{},
162+
}
163+
t.Setenv("SUPABASE_HOSTNAME", "evil.example.com")
164+
t.Setenv("SUPABASE_AUTH_SITE_URL", "http://preview.com")
165+
t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==")
166+
167+
require.NoError(t, config.Load("", fsys))
168+
assert.Equal(t, "127.0.0.1", config.Hostname)
169+
assert.Equal(t, "http://preview.com", config.Auth.SiteUrl)
170+
}
171+
157172
func TestFileSizeLimitConfigParsing(t *testing.T) {
158173
t.Run("test file size limit parsing number", func(t *testing.T) {
159174
var testConfig config

0 commit comments

Comments
 (0)