{"record":{"id":"094ae3763d2f8780","repo":"go-gitea/gitea","slug":"markup-sanitizer-rule-regexp-must-start-with-and","errorCode":null,"errorMessage":"Markup sanitizer rule regexp must start with ^ and end with $ to be strict","messagePattern":"Markup sanitizer rule regexp must start with \\^ and end with \\$ to be strict","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"modules/markup/sanitizer_custom.go","lineNumber":23,"sourceCode":"\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"gitea.dev/modules/setting\"\n\n\t\"github.com/microcosm-cc/bluemonday\"\n)\n\nfunc (st *Sanitizer) addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {\n\tfor _, rule := range rules {\n\t\tif rule.AllowDataURIImages {\n\t\t\tpolicy.AllowDataURIImages()\n\t\t}\n\t\tif rule.Element != \"\" {\n\t\t\tif rule.Regexp != \"\" {\n\t\t\t\tif !strings.HasPrefix(rule.Regexp, \"^\") || !strings.HasSuffix(rule.Regexp, \"$\") {\n\t\t\t\t\tpanic(\"Markup sanitizer rule regexp must start with ^ and end with $ to be strict\")\n\t\t\t\t}\n\t\t\t\tpolicy.AllowAttrs(rule.AllowAttr).Matching(regexp.MustCompile(rule.Regexp)).OnElements(rule.Element)\n\t\t\t} else {\n\t\t\t\tpolicy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)\n\t\t\t}\n\t\t}\n\t}\n}\n","sourceCodeStart":5,"sourceCodeEnd":32,"githubUrl":"https://github.com/go-gitea/gitea/blob/43ace7cc8ad5fa20027b1ca5b3ab5f1134972ed5/modules/markup/sanitizer_custom.go#L5-L32","documentation":"A start-time panic in Gitea's custom markup sanitizer. When app.ini defines [markup.sanitizer.*] rules with both ELEMENT and REGEXP, the code enforces that the regexp is anchored: it must begin with ^ and end with $. This strictness stops attacker-supplied markup from smuggling partial matches past the allow-list (e.g., a regexp like https?://evil\\.com matching inside a longer attribute value), so a non-anchored rule aborts startup deliberately.","triggerScenarios":"Any [markup.sanitizer.<name>] section in app.ini with ELEMENT set and a REGEXP that does not start with ^ or does not end with $: e.g. REGEXP = ^https?:// (missing trailing $), REGEXP = .* (no anchors), or trailing whitespace/newline after the $ in the quoted value.","commonSituations":"Copying a sanitizer example from an outdated blog/docs where anchors were not required; upgrading Gitea to a version that introduced the anchor enforcement against configs that previously worked; hand-editing app.ini and introducing a typo or missing anchor; trailing spaces after $ inside the value.","solutions":["Edit app.ini and anchor every sanitizer REGEXP: wrap the pattern as ^...$ (e.g. REGEXP = ^https?://example\\.com/.*$)","Restart Gitea and confirm startup succeeds","Validate all other [markup.sanitizer.*] sections for the same problem in one pass","After it boots, test the sanitizer with a crafted issue/comment containing the element to confirm the rule matches as intended"],"exampleFix":"; before\n[markup.sanitizer.example]\nELEMENT = a\nALLOW_ATTR = href\nREGEXP = https?://example\\.com/.*\n\n; after\n[markup.sanitizer.example]\nELEMENT = a\nALLOW_ATTR = href\nREGEXP = ^https?://example\\.com/.*$","handlingStrategy":"validation","validationCode":"// Validate app.ini sanitizer rules before boot (config linter / pre-flight check)\nfunc validateSanitizerRegexp(rules []setting.MarkupSanitizerRule) error {\n\tfor _, r := range rules {\n\t\tif r.Element != \"\" && r.Regexp != \"\" {\n\t\t\tif !strings.HasPrefix(r.Regexp, \"^\") || !strings.HasSuffix(r.Regexp, \"$\") {\n\t\t\t\treturn fmt.Errorf(\"markup.sanitizer rule for %q: REGEXP %q must start with ^ and end with $\", r.Element, r.Regexp)\n\t\t\t}\n\t\t\tif _, err := regexp.Compile(r.Regexp); err != nil {\n\t\t\t\treturn fmt.Errorf(\"markup.sanitizer rule for %q: invalid REGEXP: %w\", r.Element, err)\n\t\t\t}\n\t}\n\treturn nil\n}","typeGuard":"// Go: guard before applying a rule\nfunc isStrictRegexp(s string) bool {\n\treturn strings.HasPrefix(s, \"^\") && strings.HasSuffix(s, \"$\")\n}","tryCatchPattern":"// Panics happen at process start; catch them in the supervisor/deployment, not in code:\n// run `gitea doctor` / a config-dry-run in the container entrypoint before exec'ing the server\n// so a bad app.ini fails fast with a clear message instead of a crash loop.","preventionTips":["Always author sanitizer regexps fully anchored: ^pattern$ — write the anchors first, then the pattern","Lint app.ini in CI or with `gitea doctor` after every markup.sanitizer edit, before restarting production","Trim surrounding whitespace in REGEXP values; a space after $ makes the check fail mysteriously","Test the rule with both a matching and a non-matching attribute value after startup"],"tags":["config","app-ini","markup","sanitizer","panic","startup","security"],"backgroundTag":null,"analyzedSha":"43ace7cc8ad5fa20027b1ca5b3ab5f1134972ed5","analyzedAt":"2026-08-15T09:36:00.065Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}