Skip to content

Commit 8f7ad69

Browse files
akoclaude
andcommitted
Fix 10 CodeQL security alerts
- Symlink path traversal (alerts #7-10): resolve symlink effective destination and verify it stays within extraction directory, blocking absolute symlinks and relative paths that escape the root - Integer overflow (alerts #4-6): add safeInt32() with clamping to prevent silent overflow on int-to-int32 conversions in settings - Sensitive data logging (alert #3): redact Password/Secret fields before printing exported JSON in example code - Allocation overflow (alert #2): reject unreasonably large inputs in bytesToHex fallback path to prevent len(b)*2 overflow - Workflow permissions (alert #1): add explicit contents:read permission to GitHub Actions workflow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 15e636b commit 8f7ad69

5 files changed

Lines changed: 74 additions & 7 deletions

File tree

.github/workflows/push-test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ name: Build, Test & Lint
22

33
on: [push, pull_request]
44

5+
permissions:
6+
contents: read
7+
58
jobs:
69
build-and-test:
710
runs-on: ubuntu-latest

cmd/mxcli/docker/download.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,21 @@ func DownloadRuntime(version string, w io.Writer) (string, error) {
213213
return cacheDir, nil
214214
}
215215

216+
// isSymlinkWithinDir checks that a symlink's effective destination resolves
217+
// within the allowed directory. Prevents path traversal via absolute symlinks
218+
// or relative paths that escape the extraction root.
219+
func isSymlinkWithinDir(linkTarget, symlinkPath, allowedDir string) bool {
220+
var resolved string
221+
if filepath.IsAbs(linkTarget) {
222+
resolved = linkTarget
223+
} else {
224+
resolved = filepath.Join(filepath.Dir(symlinkPath), linkTarget)
225+
}
226+
resolved = filepath.Clean(resolved)
227+
cleanDir := filepath.Clean(allowedDir) + string(os.PathSeparator)
228+
return strings.HasPrefix(resolved, cleanDir) || resolved == filepath.Clean(allowedDir)
229+
}
230+
216231
// extractTarGzStrip1 extracts a tar.gz stream to the target directory,
217232
// stripping the first path component (equivalent to tar --strip-components=1).
218233
func extractTarGzStrip1(r io.Reader, targetDir string) error {
@@ -279,6 +294,10 @@ func extractTarGzStrip1(r io.Reader, targetDir string) error {
279294
if strings.Contains(linkTarget, "..") {
280295
continue
281296
}
297+
// Resolve effective destination and verify it stays within targetDir
298+
if !isSymlinkWithinDir(linkTarget, target, targetDir) {
299+
continue
300+
}
282301
os.Remove(target)
283302
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
284303
return fmt.Errorf("creating parent directory for symlink %s: %w", target, err)
@@ -347,6 +366,10 @@ func extractTarGz(r io.Reader, targetDir string) error {
347366
if strings.Contains(linkTarget, "..") {
348367
continue
349368
}
369+
// Resolve effective destination and verify it stays within targetDir
370+
if !isSymlinkWithinDir(linkTarget, target, targetDir) {
371+
continue
372+
}
350373
os.Remove(target) // Remove existing if any
351374
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
352375
return fmt.Errorf("creating parent directory for symlink %s: %w", target, err)

examples/read_project/main.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,35 @@ import (
1010
"encoding/json"
1111
"fmt"
1212
"os"
13+
"strings"
1314

1415
"github.com/mendixlabs/mxcli"
1516
)
1617

18+
// redactSensitiveFields recursively walks a JSON map and replaces values
19+
// for keys containing "Password" or "Secret" with "***".
20+
func redactSensitiveFields(m map[string]any) {
21+
for key, val := range m {
22+
lk := strings.ToLower(key)
23+
if strings.Contains(lk, "password") || strings.Contains(lk, "secret") {
24+
if _, ok := val.(string); ok {
25+
m[key] = "***"
26+
}
27+
continue
28+
}
29+
switch v := val.(type) {
30+
case map[string]any:
31+
redactSensitiveFields(v)
32+
case []any:
33+
for _, item := range v {
34+
if nested, ok := item.(map[string]any); ok {
35+
redactSensitiveFields(nested)
36+
}
37+
}
38+
}
39+
}
40+
}
41+
1742
func main() {
1843
if len(os.Args) < 2 {
1944
fmt.Println("Usage: read_project <path-to-mpr-file>")
@@ -239,6 +264,7 @@ func main() {
239264
} else {
240265
var prettyJSON map[string]any
241266
json.Unmarshal(jsonData, &prettyJSON)
267+
redactSensitiveFields(prettyJSON)
242268
output, _ := json.MarshalIndent(prettyJSON, "", " ")
243269
fmt.Println(string(output))
244270
}

mdl/executor/cmd_pages_builder_v3_pluggable.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,9 @@ func hexByte(c byte) byte {
989989
func bytesToHex(b []byte) string {
990990
if len(b) != 16 {
991991
// Fallback for non-standard lengths
992+
if len(b) > 1024 {
993+
return "" // reject unreasonably large inputs
994+
}
992995
const hexChars = "0123456789abcdef"
993996
result := make([]byte, len(b)*2)
994997
for i, v := range b {

sdk/mpr/writer_settings.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@ package mpr
44

55
import (
66
"fmt"
7+
"math"
78

89
"github.com/mendixlabs/mxcli/model"
910

1011
"go.mongodb.org/mongo-driver/bson"
1112
)
1213

14+
// safeInt32 converts an int to int32 with clamping to prevent silent overflow.
15+
func safeInt32(v int) int32 {
16+
if v > math.MaxInt32 {
17+
return math.MaxInt32
18+
}
19+
if v < math.MinInt32 {
20+
return math.MinInt32
21+
}
22+
return int32(v)
23+
}
24+
1325
// UpdateProjectSettings updates the project settings document.
1426
// The project settings document always exists, so this only needs update, not create/delete.
1527
func (w *Writer) UpdateProjectSettings(ps *model.ProjectSettings) error {
@@ -77,12 +89,12 @@ func serializeModelSettings(ms *model.ModelSettings, raw map[string]any) map[str
7789
raw["HealthCheckMicroflow"] = ms.HealthCheckMicroflow
7890
raw["AllowUserMultipleSessions"] = ms.AllowUserMultipleSessions
7991
raw["HashAlgorithm"] = ms.HashAlgorithm
80-
raw["BcryptCost"] = int32(ms.BcryptCost)
92+
raw["BcryptCost"] = safeInt32(ms.BcryptCost)
8193
raw["JavaVersion"] = ms.JavaVersion
8294
raw["RoundingMode"] = ms.RoundingMode
8395
raw["ScheduledEventTimeZoneCode"] = ms.ScheduledEventTimeZoneCode
8496
raw["FirstDayOfWeek"] = ms.FirstDayOfWeek
85-
raw["DecimalScale"] = int32(ms.DecimalScale)
97+
raw["DecimalScale"] = safeInt32(ms.DecimalScale)
8698
raw["EnableDataStorageOptimisticLocking"] = ms.EnableDataStorageOptimisticLocking
8799
raw["UseDatabaseForeignKeyConstraints"] = ms.UseDatabaseForeignKeyConstraints
88100
return raw
@@ -108,10 +120,10 @@ func serializeServerConfiguration(cfg *model.ServerConfiguration) bson.M {
108120
"DatabaseUserName": cfg.DatabaseUserName,
109121
"DatabasePassword": cfg.DatabasePassword,
110122
"DatabaseUseIntegratedSecurity": cfg.DatabaseUseIntegratedSecurity,
111-
"HttpPortNumber": int32(cfg.HttpPortNumber),
112-
"ServerPortNumber": int32(cfg.ServerPortNumber),
123+
"HttpPortNumber": safeInt32(cfg.HttpPortNumber),
124+
"ServerPortNumber": safeInt32(cfg.ServerPortNumber),
113125
"ApplicationRootUrl": cfg.ApplicationRootUrl,
114-
"MaxJavaHeapSize": int32(cfg.MaxJavaHeapSize),
126+
"MaxJavaHeapSize": safeInt32(cfg.MaxJavaHeapSize),
115127
"ExtraJvmParameters": cfg.ExtraJvmParameters,
116128
"OpenAdminPort": cfg.OpenAdminPort,
117129
"OpenHttpPort": cfg.OpenHttpPort,
@@ -160,7 +172,7 @@ func serializeLanguageSettings(ls *model.LanguageSettings, raw map[string]any) m
160172
// serializeWorkflowsSettings updates the raw BSON map with modified workflow settings.
161173
func serializeWorkflowsSettings(ws *model.WorkflowsSettings, raw map[string]any) map[string]any {
162174
raw["UserEntity"] = ws.UserEntity
163-
raw["DefaultTaskParallelism"] = int32(ws.DefaultTaskParallelism)
164-
raw["WorkflowEngineParallelism"] = int32(ws.WorkflowEngineParallelism)
175+
raw["DefaultTaskParallelism"] = safeInt32(ws.DefaultTaskParallelism)
176+
raw["WorkflowEngineParallelism"] = safeInt32(ws.WorkflowEngineParallelism)
165177
return raw
166178
}

0 commit comments

Comments
 (0)