Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ on:
required: true
default: 'v0.3.20'

permissions:
contents: read

jobs:
test-dispatch:
name: Test Repository Dispatch
Expand Down
5 changes: 5 additions & 0 deletions schema/reflect.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package schema

import (
"math"
"reflect"
"strconv"
"strings"
Expand Down Expand Up @@ -129,6 +130,10 @@ func parseDefault(s string) any {
return false
}
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
// Guard against overflow when converting int64 → int on 32-bit platforms.
if i < math.MinInt || i > math.MaxInt {
return s // out-of-range: preserve as string
}
return int(i)
}
if f, err := strconv.ParseFloat(s, 64); err == nil {
Expand Down
38 changes: 38 additions & 0 deletions schema/reflect_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package schema

import (
"math"
"strconv"
"testing"
)

Expand Down Expand Up @@ -269,3 +271,39 @@ func TestParseDefault(t *testing.T) {
}
}
}

// TestParseDefaultIntOverflow verifies that integer values outside the range of
// int on the current platform are never returned as int (which would silently
// overflow on 32-bit hosts). On 64-bit hosts this exercises the guard for
// hypothetical 32-bit deployment by directly testing the bounds.
func TestParseDefaultIntOverflow(t *testing.T) {
// Values within [MinInt, MaxInt] must parse as int.
maxInt := strconv.FormatInt(math.MaxInt, 10)
gotMax := parseDefault(maxInt)
if _, ok := gotMax.(int); !ok {
t.Errorf("parseDefault(%q) should return int, got %T", maxInt, gotMax)
}
if gotMax != math.MaxInt {
t.Errorf("parseDefault(%q) = %v, want %v", maxInt, gotMax, math.MaxInt)
}

minInt := strconv.FormatInt(math.MinInt, 10)
gotMin := parseDefault(minInt)
if _, ok := gotMin.(int); !ok {
t.Errorf("parseDefault(%q) should return int, got %T", minInt, gotMin)
}
if gotMin != math.MinInt {
t.Errorf("parseDefault(%q) = %v, want %v", minInt, gotMin, math.MinInt)
}

// On 32-bit platforms (math.MaxInt == math.MaxInt32), values between
// math.MaxInt32+1 and math.MaxInt64 must NOT return as int.
if math.MaxInt == math.MaxInt32 {
overflowVal := int64(math.MaxInt32) + 1
overflowStr := strconv.FormatInt(overflowVal, 10)
got := parseDefault(overflowStr)
if _, ok := got.(int); ok {
t.Errorf("parseDefault(%q) returned int on 32-bit platform, expected non-int to avoid overflow", overflowStr)
}
}
}
58 changes: 58 additions & 0 deletions store/api_keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package store

import (
"context"
"encoding/hex"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -500,3 +501,60 @@ func TestSQLiteAPIKeyStoreFromDB(t *testing.T) {
t.Errorf("Name: got %q, want %q", validated.Name, "from-db-test")
}
}

// TestHashKeyUsesSHA256 verifies that hashKey produces a full SHA-256 hex digest
// (64 hex characters = 256 bits). This acts as a regression guard: if the
// implementation were ever downgraded to MD5 (32 chars) or SHA-1 (40 chars),
// this test would catch it immediately.
func TestHashKeyUsesSHA256(t *testing.T) {
const wantHexLen = 64 // SHA-256 = 32 bytes = 64 hex chars

inputs := []string{
"wf_0000000000000000000000000000dead",
"wf_" + strings.Repeat("a", 32),
"",
}
for _, in := range inputs {
h := hashKey(in)
if len(h) != wantHexLen {
t.Errorf("hashKey(%q): got len %d, want %d (SHA-256)", in, len(h), wantHexLen)
}
if _, err := hex.DecodeString(h); err != nil {
t.Errorf("hashKey(%q): output is not valid hex: %v", in, err)
}
}

// Same input must always produce the same digest (deterministic).
const key = "wf_deterministic_key_test_value00"
h1, h2 := hashKey(key), hashKey(key+"") // two separate calls
if h1 != h2 {
t.Errorf("hashKey is not deterministic: %q != %q", h1, h2)
}

// Different inputs must produce different digests.
if hashKey("wf_aaa") == hashKey("wf_bbb") {
t.Error("hashKey collision on distinct inputs")
}
}

// TestConstantTimeHashCompare verifies the constant-time equality helper used
// during API-key validation to prevent timing-oracle attacks.
func TestConstantTimeHashCompare(t *testing.T) {
a := hashKey("wf_" + strings.Repeat("x", 32))
b := hashKey("wf_" + strings.Repeat("x", 32))
c := hashKey("wf_" + strings.Repeat("y", 32))

if !constantTimeHashCompare(a, b) {
t.Error("identical hashes should compare equal")
}
if constantTimeHashCompare(a, c) {
t.Error("different hashes should not compare equal")
}
// Empty strings are equal to each other and not equal to a real hash.
if !constantTimeHashCompare("", "") {
t.Error("empty strings should compare equal")
}
if constantTimeHashCompare(a, "") {
t.Error("hash should not equal empty string")
}
}
Loading