Harden CodeQL storage and integer boundaries

This commit is contained in:
rcourtman
2026-07-09 17:37:08 +01:00
parent 24b2e40e92
commit 92524e1c27
9 changed files with 190 additions and 26 deletions
@@ -0,0 +1,7 @@
name: rcourtman/pulse-security-models
version: 0.0.1
library: true
extensionTargets:
codeql/go-all: '*'
dataExtensions:
- models/**/*.yml
@@ -0,0 +1,14 @@
extensions:
- addsTo:
pack: codeql/go-all
extensible: barrierModel
data:
- ["github.com/rcourtman/pulse-go-rewrite/internal/securityutil", "", false, "HashedStorageName", "", "", "ReturnValue", "path-injection", "manual"]
- ["github.com/rcourtman/pulse-go-rewrite/internal/securityutil", "", false, "JoinStorageLeaf", "", "", "ReturnValue[0]", "path-injection", "manual"]
- ["github.com/rcourtman/pulse-go-rewrite/internal/securityutil", "", false, "NormalizeStorageDir", "", "", "ReturnValue[0]", "path-injection", "manual"]
- ["github.com/rcourtman/pulse-go-rewrite/internal/securityutil", "", false, "ValidateOutboundFetchURL", "", "", "ReturnValue[0]", "request-forgery", "manual"]
- addsTo:
pack: codeql/go-all
extensible: barrierGuardModel
data:
- ["github.com/rcourtman/pulse-go-rewrite/internal/config", "", false, "isValidOrgID", "", "", "Argument[0]", "true", "path-injection", "manual"]
+34 -6
View File
@@ -16,6 +16,7 @@ import (
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/crypto"
"github.com/rcourtman/pulse-go-rewrite/internal/securityutil"
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
"github.com/rs/zerolog/log"
)
@@ -233,7 +234,11 @@ func (s *FileBillingStore) SaveBillingState(orgID string, state *pkglicensing.Bi
}
func (s *FileBillingStore) billingCryptoManager() (*crypto.CryptoManager, error) {
return crypto.NewCryptoManagerAt(s.resolveDataDir())
dataDir, err := s.billingBaseDataDir()
if err != nil {
return nil, err
}
return crypto.NewCryptoManagerAt(dataDir)
}
func (s *FileBillingStore) billingCryptoManagerForSecrets(values ...string) (*crypto.CryptoManager, error) {
@@ -250,22 +255,45 @@ func (s *FileBillingStore) billingStatePath(orgID string) (string, error) {
if !isValidOrgID(orgID) {
return "", fmt.Errorf("invalid organization ID: %s", orgID)
}
baseDir, err := s.billingBaseDataDir()
if err != nil {
return "", err
}
// Default org stores config at the root data dir for backward compatibility,
// so billing state for the default org must live alongside other root configs.
if orgID == "default" {
return filepath.Join(s.resolveDataDir(), "billing.json"), nil
return securityutil.JoinStorageLeaf(baseDir, "billing.json")
}
return filepath.Join(s.resolveDataDir(), "orgs", orgID, "billing.json"), nil
orgsDir, err := securityutil.JoinStorageLeaf(baseDir, "orgs")
if err != nil {
return "", fmt.Errorf("resolve orgs directory: %w", err)
}
orgDir, err := securityutil.JoinStorageLeaf(orgsDir, orgID)
if err != nil {
return "", fmt.Errorf("resolve organization billing directory: %w", err)
}
return securityutil.JoinStorageLeaf(orgDir, "billing.json")
}
func (s *FileBillingStore) resolveDataDir() string {
return ResolveRuntimeDataDir(s.baseDataDir)
func (s *FileBillingStore) billingBaseDataDir() (string, error) {
dataDir, err := securityutil.NormalizeStorageDir(ResolveRuntimeDataDir(s.baseDataDir))
if err != nil {
return "", fmt.Errorf("resolve billing data directory: %w", err)
}
return dataDir, nil
}
// loadHMACKey derives a purpose-specific HMAC key from the .encryption.key file.
// Returns an error if the key file is missing or invalid (graceful degradation).
func (s *FileBillingStore) loadHMACKey() ([]byte, error) {
keyPath := filepath.Join(s.resolveDataDir(), ".encryption.key")
dataDir, err := s.billingBaseDataDir()
if err != nil {
return nil, err
}
keyPath, err := securityutil.JoinStorageLeaf(dataDir, ".encryption.key")
if err != nil {
return nil, err
}
raw, err := os.ReadFile(keyPath)
if err != nil {
return nil, err
+54
View File
@@ -7,6 +7,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -88,6 +89,59 @@ func TestBillingState_RoundTrip(t *testing.T) {
assert.Equal(t, endsAt, *loaded.TrialEndsAt)
}
func TestBillingState_InvalidOrgIDsRejected(t *testing.T) {
dir := t.TempDir()
store := NewFileBillingStore(dir)
invalidIDs := []string{
"",
".",
"..",
"../bad",
"bad/..",
"bad/../evil",
"bad org",
"bad\torg",
"bad\norg",
"bad\\org",
"bad:org",
strings.Repeat("a", 65),
}
for _, orgID := range invalidIDs {
err := store.SaveBillingState(orgID, &entitlements.BillingState{})
require.Error(t, err, "SaveBillingState should reject orgID %q", orgID)
_, err = store.GetBillingState(orgID)
require.Error(t, err, "GetBillingState should reject orgID %q", orgID)
}
if _, err := os.Stat(filepath.Join(dir, "orgs")); !os.IsNotExist(err) {
t.Fatalf("unexpected orgs directory state after invalid org IDs: %v", err)
}
}
func TestBillingState_NonDefaultOrgUsesCanonicalStoragePath(t *testing.T) {
root := t.TempDir()
rawBaseDir := filepath.Join(root, "billing") + string(os.PathSeparator) + ".." + string(os.PathSeparator) + "billing"
expectedBaseDir := filepath.Clean(rawBaseDir)
store := NewFileBillingStore(" " + rawBaseDir + " ")
state := &entitlements.BillingState{
Capabilities: []string{"relay"},
SubscriptionState: entitlements.SubStateTrial,
}
require.NoError(t, store.SaveBillingState("acme.prod-1", state))
billingPath := filepath.Join(expectedBaseDir, "orgs", "acme.prod-1", "billing.json")
require.FileExists(t, billingPath)
loaded, err := store.GetBillingState("acme.prod-1")
require.NoError(t, err)
require.NotNil(t, loaded)
assert.Contains(t, loaded.Capabilities, "relay")
}
func TestBillingState_EncryptsHostedEntitlementSecretsAtRest(t *testing.T) {
dir := t.TempDir()
t.Setenv("PULSE_LEGACY_KEY_PATH", filepath.Join(t.TempDir(), ".encryption.key"))
+2 -2
View File
@@ -1337,9 +1337,9 @@ func parseSMARTOutput(output []byte, target smartctlTarget) (*DiskSMART, error)
} else {
for _, attr := range smartData.ATASmartAttributes.Table {
if attr.ID == 194 || attr.ID == 190 {
temp := int(parseRawValue(attr.Raw.String, attr.Raw.Value))
temp := parseRawValue(attr.Raw.String, attr.Raw.Value)
if temp > 0 && temp < 150 {
result.Temperature = temp
result.Temperature = int(temp)
break
}
}
+27
View File
@@ -478,6 +478,33 @@ func TestParseSMARTOutputFallsBackToOriginalTextTemperature(t *testing.T) {
}
}
func TestParseSMARTOutputRejectsOutOfRangeATARawTemperature(t *testing.T) {
payload := []byte(`{
"device": {"protocol": "ATA"},
"model_name": "WDC WD40EFRX",
"serial_number": "WD-999",
"smart_status": {"passed": true},
"ata_smart_attributes": {
"table": [{
"id": 194,
"name": "Temperature_Celsius",
"raw": {"value": 9223372036854775807, "string": "9223372036854775807"}
}]
}
}`)
result, err := parseSMARTOutput(payload, smartctlTarget{Path: "/dev/ada0"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result")
}
if result.Temperature != 0 {
t.Fatalf("expected out-of-range ATA raw temperature to be ignored, got %#v", result)
}
}
func TestParseSMARTOutputFallsBackToPlainTextOutput(t *testing.T) {
output := []byte(`
=== START OF INFORMATION SECTION ===
+4 -4
View File
@@ -1502,7 +1502,7 @@ func appendDiskTemperature(out map[string]int, diskName string, value any) {
}
}
temperature, ok := parseInt64Any(value)
if !ok || temperature <= 0 {
if !ok || temperature <= 0 || temperature >= 150 {
return
}
out[diskName] = int(temperature)
@@ -2887,7 +2887,7 @@ func readIntAny(record map[string]any, keys ...string) int {
if !ok || value == nil {
continue
}
if parsed, ok := parseInt64Any(value); ok {
if parsed, ok := parseInt64Any(value); ok && parsed >= math.MinInt32 && parsed <= math.MaxInt32 {
return int(parsed)
}
}
@@ -2907,13 +2907,13 @@ func readIntSliceAny(record map[string]any, keys ...string) []int {
case []any:
out := make([]int, 0, len(typed))
for _, item := range typed {
if parsed, ok := parseInt64Any(item); ok {
if parsed, ok := parseInt64Any(item); ok && parsed >= math.MinInt32 && parsed <= math.MaxInt32 {
out = append(out, int(parsed))
}
}
return out
default:
if parsed, ok := parseInt64Any(value); ok {
if parsed, ok := parseInt64Any(value); ok && parsed >= math.MinInt32 && parsed <= math.MaxInt32 {
return []int{int(parsed)}
}
}
+40
View File
@@ -173,6 +173,46 @@ func TestParseInt64FromAnyCoversBranches(t *testing.T) {
}
}
func TestReadIntHelpersSkipOutOfRangeValues(t *testing.T) {
record := map[string]any{
"overflow": "2147483648",
"valid": "42",
"values": []any{
"1",
"2147483648",
"-2147483649",
"2",
},
}
if got := readIntAny(record, "overflow", "valid"); got != 42 {
t.Fatalf("readIntAny() = %d, want fallback value 42", got)
}
got := readIntSliceAny(record, "values")
if len(got) != 2 || got[0] != 1 || got[1] != 2 {
t.Fatalf("readIntSliceAny() = %#v, want []int{1, 2}", got)
}
}
func TestAppendDiskTemperatureRejectsOutOfRangeValues(t *testing.T) {
temperatures := map[string]int{}
appendDiskTemperature(temperatures, "sda", int64(149))
appendDiskTemperature(temperatures, "sdb", int64(150))
appendDiskTemperature(temperatures, "sdc", "2147483648")
if got := temperatures["sda"]; got != 149 {
t.Fatalf("expected bounded valid temperature 149, got %d", got)
}
if _, ok := temperatures["sdb"]; ok {
t.Fatal("expected temperature 150 to be rejected")
}
if _, ok := temperatures["sdc"]; ok {
t.Fatal("expected oversized temperature to be rejected")
}
}
func TestParseBoolFromAnyCoversBranches(t *testing.T) {
tests := []struct {
name string
+8 -14
View File
@@ -80,31 +80,25 @@ func parseFlexibleIntString(raw string) (int, error) {
return floatToIntTrunc(parsed)
}
parsed, err := strconv.ParseInt(raw, 10, 64)
parsed, err := strconv.Atoi(raw)
if err != nil {
return 0, err
}
return int64ToInt(parsed)
return parsed, nil
}
func int64ToInt(v int64) (int, error) {
if strconv.IntSize == 32 {
if v > math.MaxInt32 || v < math.MinInt32 {
return 0, fmt.Errorf("integer %d exceeds int range", v)
}
return int(int32(v)), nil
maxInt := int64(int(^uint(0) >> 1))
minInt := -maxInt - 1
if v > maxInt || v < minInt {
return 0, fmt.Errorf("integer %d exceeds int range", v)
}
return int(v), nil
}
func uint64ToInt(v uint64) (int, error) {
if strconv.IntSize == 32 {
if v > math.MaxInt32 {
return 0, fmt.Errorf("integer %d exceeds int range", v)
}
return int(int32(v)), nil
}
if v > math.MaxInt64 {
maxInt := uint64(^uint(0) >> 1)
if v > maxInt {
return 0, fmt.Errorf("integer %d exceeds int range", v)
}
return int(v), nil