fix: dedup env var keys, add IDOR test, enforce body size limit
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:
clawbot
2026-03-10 18:14:20 -07:00
parent 609ce1d0d3
commit eaf3d48eae
2 changed files with 186 additions and 21 deletions

View File

@@ -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{