Skip to content

Commit 14d855c

Browse files
committed
chore(otp): set a clamp to prevent invalid values for otp expiration
1 parent fcaf26e commit 14d855c

2 files changed

Lines changed: 82 additions & 2 deletions

File tree

internal/conf/configuration.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"errors"
99
"fmt"
10+
"math"
1011
"net/url"
1112
"regexp"
1213
"strings"
@@ -25,6 +26,12 @@ const defaultChallengeExpiryDuration float64 = 300
2526
const defaultFactorExpiryDuration time.Duration = 300 * time.Second
2627
const defaultFlowStateExpiryDuration time.Duration = 300 * time.Second
2728

29+
// maxOtpExp is the largest OTP validity window, in seconds, that survives
30+
// conversion to a time.Duration, which counts nanoseconds in an int64. That is
31+
// roughly 292 years. ApplyDefaults clamps OtpExp to it so the conversion cannot
32+
// overflow into a negative duration, which would expire tokens at creation.
33+
const maxOtpExp uint = math.MaxInt64 / uint(time.Second)
34+
2835
// See: https://www.postgresql.org/docs/7.0/syntax525.htm
2936
var postgresNamesRegexp = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]{0,62}$`)
3037

@@ -696,7 +703,8 @@ type MailerConfiguration struct {
696703
}
697704

698705
func (c *MailerConfiguration) OtpExpAsDuration() time.Duration {
699-
return time.Duration(c.OtpExp) * time.Second // #nosec G115 -- OtpExp comes from trusted config, not user input
706+
// OtpExp is clamped to maxOtpExp in ApplyDefaults, so this cannot overflow.
707+
return time.Duration(c.OtpExp) * time.Second // #nosec G115
700708
}
701709

702710
func (c *MailerConfiguration) Validate() error {
@@ -789,7 +797,8 @@ type SmsProviderConfiguration struct {
789797
}
790798

791799
func (c *SmsProviderConfiguration) OtpExpAsDuration() time.Duration {
792-
return time.Duration(c.OtpExp) * time.Second // #nosec G115 -- OtpExp comes from trusted config, not user input
800+
// OtpExp is clamped to maxOtpExp in ApplyDefaults, so this cannot overflow.
801+
return time.Duration(c.OtpExp) * time.Second // #nosec G115
793802
}
794803

795804
func (c *SmsProviderConfiguration) GetTestOTP(phone string, now time.Time) (string, bool) {
@@ -1211,6 +1220,10 @@ func (config *GlobalConfiguration) ApplyDefaults() error {
12111220
config.Mailer.OtpExp = 86400 // 1 day
12121221
}
12131222

1223+
if config.Mailer.OtpExp > maxOtpExp {
1224+
config.Mailer.OtpExp = maxOtpExp
1225+
}
1226+
12141227
if config.Mailer.OtpLength == 0 || config.Mailer.OtpLength < 6 || config.Mailer.OtpLength > 10 {
12151228
// 6-digit otp by default
12161229
config.Mailer.OtpLength = 6
@@ -1228,6 +1241,10 @@ func (config *GlobalConfiguration) ApplyDefaults() error {
12281241
config.Sms.OtpExp = 60
12291242
}
12301243

1244+
if config.Sms.OtpExp > maxOtpExp {
1245+
config.Sms.OtpExp = maxOtpExp
1246+
}
1247+
12311248
if config.Sms.OtpLength == 0 || config.Sms.OtpLength < 6 || config.Sms.OtpLength > 10 {
12321249
// 6-digit otp by default
12331250
config.Sms.OtpLength = 6

internal/conf/configuration_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package conf
33
import (
44
"encoding/base64"
55
"errors"
6+
"math"
67
"os"
78
"sort"
89
"strings"
@@ -1251,3 +1252,65 @@ func TestProviderLinkingDomainsDecode(t *testing.T) {
12511252
require.Error(t, d.Decode("custom:github="))
12521253
}
12531254
}
1255+
1256+
func TestApplyDefaultsClampsOtpExp(t *testing.T) {
1257+
baseConfig := func() *GlobalConfiguration {
1258+
c := &GlobalConfiguration{}
1259+
c.JWT.Secret = "secret"
1260+
return c
1261+
}
1262+
1263+
cases := []struct {
1264+
desc string
1265+
mailerExp uint
1266+
smsExp uint
1267+
wantMailer uint
1268+
wantSms uint
1269+
}{
1270+
{
1271+
desc: "zero takes the per-channel default",
1272+
mailerExp: 0,
1273+
smsExp: 0,
1274+
wantMailer: 86400,
1275+
wantSms: 60,
1276+
},
1277+
{
1278+
desc: "in-range values pass through untouched",
1279+
mailerExp: 3600,
1280+
smsExp: 120,
1281+
wantMailer: 3600,
1282+
wantSms: 120,
1283+
},
1284+
{
1285+
desc: "the ceiling itself is not clamped",
1286+
mailerExp: maxOtpExp,
1287+
smsExp: maxOtpExp,
1288+
wantMailer: maxOtpExp,
1289+
wantSms: maxOtpExp,
1290+
},
1291+
{
1292+
desc: "values above the ceiling clamp down to it",
1293+
mailerExp: maxOtpExp + 1,
1294+
smsExp: math.MaxUint,
1295+
wantMailer: maxOtpExp,
1296+
wantSms: maxOtpExp,
1297+
},
1298+
}
1299+
1300+
for _, c := range cases {
1301+
t.Run(c.desc, func(t *testing.T) {
1302+
cfg := baseConfig()
1303+
cfg.Mailer.OtpExp = c.mailerExp
1304+
cfg.Sms.OtpExp = c.smsExp
1305+
require.NoError(t, cfg.ApplyDefaults())
1306+
1307+
require.Equal(t, c.wantMailer, cfg.Mailer.OtpExp)
1308+
require.Equal(t, c.wantSms, cfg.Sms.OtpExp)
1309+
1310+
// the clamp exists so that this conversion cannot overflow into a
1311+
// negative duration, which would expire every token at creation
1312+
require.Positive(t, cfg.Mailer.OtpExpAsDuration())
1313+
require.Positive(t, cfg.Sms.OtpExpAsDuration())
1314+
})
1315+
}
1316+
}

0 commit comments

Comments
 (0)