{"record":{"id":"12f9ee73c72eca79","repo":"caddyserver/caddy","slug":"too-many-unclosed-placeholders","errorCode":null,"errorMessage":"too many unclosed placeholders","messagePattern":"too many unclosed placeholders","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"replacer.go","lineNumber":212,"sourceCode":"\nscan:\n\tfor i := 0; i < len(input); i++ {\n\t\t// check for escaped braces\n\t\tif i > 0 && input[i-1] == phEscape && (input[i] == phClose || input[i] == phOpen) {\n\t\t\tsb.WriteString(input[lastWriteCursor : i-1])\n\t\t\tlastWriteCursor = i\n\t\t\tcontinue\n\t\t}\n\n\t\tif input[i] != phOpen {\n\t\t\tcontinue\n\t\t}\n\n\t\t// our iterator is now on an unescaped open brace (start of placeholder)\n\n\t\t// too many unclosed placeholders in absolutely ridiculous input can be extremely slow (issue #4170)\n\t\tif unclosedCount > 100 {\n\t\t\treturn \"\", fmt.Errorf(\"too many unclosed placeholders\")\n\t\t}\n\n\t\t// find the end of the placeholder\n\t\tend := strings.Index(input[i:], string(phClose)) + i\n\t\tif end < i {\n\t\t\tunclosedCount++\n\t\t\tcontinue\n\t\t}\n\n\t\t// if necessary look for the first closing brace that is not escaped\n\t\tfor end > 0 && end < len(input)-1 && input[end-1] == phEscape {\n\t\t\tnextEnd := strings.Index(input[end+1:], string(phClose))\n\t\t\tif nextEnd < 0 {\n\t\t\t\tunclosedCount++\n\t\t\t\tcontinue scan\n\t\t\t}\n\t\t\tend += nextEnd + 1\n\t\t}","sourceCodeStart":194,"sourceCodeEnd":230,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/replacer.go#L194-L230","documentation":"The core replacer (used to expand {placeholders}) aborts when it encounters more than 100 open braces with no matching close brace in the input. This guards against catastrophic quadratic scanning on pathological input (issue #4170) — a flood of '{' characters would otherwise make replacement extremely slow.","triggerScenarios":"Calling repl.Replace()/ReplaceOrErr (directly or via any config field that goes through the replacer) on input containing 100+ unmatched '{' characters; e.g. logging or proxying a header value full of braces into a replaced field.","commonSituations":"Client-controlled data (headers, paths, bodies) flowing into placeholder-substituted config values; templating code that concatenates user input with literal braces; JSON snippets passed through the replacer.","solutions":["Escape literal braces with a backslash (\\{ and \\}) so they are not treated as placeholder opens","Sanitize user-supplied input before feeding it into replaced values (strip or escape '{')","Upgrade Caddy if on an old version predating the fix for #4170"],"exampleFix":"// before\ntpl := \"data: \" + userInput // userInput has 100+ '{'\nval := repl.ReplaceKnown(tpl, \"\")\n\n// after\nimport \"strings\"\ntpl := \"data: \" + strings.ReplaceAll(strings.ReplaceAll(userInput, \"{\", \"\\\\{\"), \"}\", \"\\\\}\")\nval := repl.ReplaceKnown(tpl, \"\")","handlingStrategy":"validation","validationCode":"if strings.Count(input, \"{\")-strings.Count(input, \"}\") > 100 {\n    return fmt.Errorf(\"input has too many unmatched open braces\")\n}","typeGuard":null,"tryCatchPattern":"if out, err := repl.ReplaceOrErr(input, false, false); err != nil {\n    if strings.Contains(err.Error(), \"too many unclosed placeholders\") {\n        out = sanitizeBraces(input) // escape braces and retry\n    }\n}","preventionTips":["Escape literal braces as \\{ and \\} in replaced strings","Never feed untrusted raw text into placeholder-substituted fields"],"tags":["caddy","replacer","placeholders","dos-protection"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}