fix: dedup env var keys, add IDOR test, enforce body size limit
All checks were successful
Check / check (pull_request) Successful in 3m30s
All checks were successful
Check / check (pull_request) Successful in 3m30s
- Server-side duplicate key dedup (last wins) via deduplicateEnvPairs helper - Cross-app isolation test verifying env var save scopes by app_id - http.MaxBytesReader wraps request body with 1MB limit - Test for oversized body rejection (400)
This commit is contained in:
@@ -909,10 +909,39 @@ type envPairJSON struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// envVarMaxBodyBytes is the maximum allowed request body size for env var saves (1 MB).
|
||||
const envVarMaxBodyBytes = 1 << 20
|
||||
|
||||
// deduplicateEnvPairs validates and deduplicates env var pairs.
|
||||
// It rejects empty keys (returns a non-empty error string) and
|
||||
// deduplicates by key, keeping the last occurrence.
|
||||
func deduplicateEnvPairs(pairs []envPairJSON) ([]models.EnvVarPair, string) {
|
||||
seen := make(map[string]int, len(pairs))
|
||||
|
||||
var result []models.EnvVarPair
|
||||
|
||||
for _, p := range pairs {
|
||||
trimmedKey := strings.TrimSpace(p.Key)
|
||||
if trimmedKey == "" {
|
||||
return nil, "empty environment variable key is not allowed"
|
||||
}
|
||||
|
||||
if idx, exists := seen[trimmedKey]; exists {
|
||||
result[idx] = models.EnvVarPair{Key: trimmedKey, Value: p.Value}
|
||||
} else {
|
||||
seen[trimmedKey] = len(result)
|
||||
result = append(result, models.EnvVarPair{Key: trimmedKey, Value: p.Value})
|
||||
}
|
||||
}
|
||||
|
||||
return result, ""
|
||||
}
|
||||
|
||||
// HandleEnvVarSave handles bulk saving of all environment variables.
|
||||
// It reads a JSON array of {key, value} objects from the request body,
|
||||
// deletes all existing env vars for the app, and inserts the full
|
||||
// submitted set atomically within a database transaction.
|
||||
// Duplicate keys are deduplicated server-side (last occurrence wins).
|
||||
func (h *Handlers) HandleEnvVarSave() http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
appID := chi.URLParam(request, "id")
|
||||
@@ -924,38 +953,32 @@ func (h *Handlers) HandleEnvVarSave() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Limit request body size to prevent abuse
|
||||
request.Body = http.MaxBytesReader(writer, request.Body, envVarMaxBodyBytes)
|
||||
|
||||
var pairs []envPairJSON
|
||||
|
||||
decodeErr := json.NewDecoder(request.Body).Decode(&pairs)
|
||||
if decodeErr != nil {
|
||||
http.Error(writer, "Bad Request", http.StatusBadRequest)
|
||||
h.respondJSON(writer, request, map[string]string{
|
||||
"error": "invalid request body",
|
||||
}, http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Validate: reject entries with empty keys
|
||||
var modelPairs []models.EnvVarPair
|
||||
modelPairs, validationErr := deduplicateEnvPairs(pairs)
|
||||
if validationErr != "" {
|
||||
h.respondJSON(writer, request, map[string]string{
|
||||
"error": validationErr,
|
||||
}, http.StatusBadRequest)
|
||||
|
||||
for _, p := range pairs {
|
||||
trimmedKey := strings.TrimSpace(p.Key)
|
||||
if trimmedKey == "" {
|
||||
h.respondJSON(writer, request, map[string]string{
|
||||
"error": "empty environment variable key is not allowed",
|
||||
}, http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
modelPairs = append(modelPairs, models.EnvVarPair{
|
||||
Key: trimmedKey,
|
||||
Value: p.Value,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Atomically replace all env vars in a transaction
|
||||
ctx := request.Context()
|
||||
|
||||
replaceErr := models.ReplaceEnvVarsByAppID(ctx, h.db, application.ID, modelPairs)
|
||||
replaceErr := models.ReplaceEnvVarsByAppID(
|
||||
request.Context(), h.db, application.ID, modelPairs,
|
||||
)
|
||||
if replaceErr != nil {
|
||||
h.log.Error("failed to replace env vars", "error", replaceErr)
|
||||
h.respondJSON(writer, request, map[string]string{
|
||||
|
||||
Reference in New Issue
Block a user