{"record":{"id":"2a16c9afa66f516d","repo":"juanfont/headscale","slug":"s-confirm-form-unterminated-action-attribute","errorCode":null,"errorMessage":"%s confirm form: unterminated action attribute","messagePattern":"(.+?) confirm form: unterminated action attribute","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"integration/scenario.go","lineNumber":1263,"sourceCode":"// POSTs the form using the same HTTP client (which carries the CSRF\n// cookie set by the callback).\nfunc submitConfirmForm(\n\thostname string,\n\thtmlBody string,\n\tprevResp *http.Response,\n\thc *http.Client,\n) (string, *url.URL, error) {\n\t// Extract form action URL.\n\tactionIdx := strings.Index(htmlBody, `action=\"`)\n\tif actionIdx == -1 {\n\t\treturn \"\", nil, fmt.Errorf(\"%s confirm form: no action attribute\", hostname) //nolint:err113\n\t}\n\n\tactionStart := actionIdx + len(`action=\"`)\n\n\tactionEnd := strings.Index(htmlBody[actionStart:], `\"`)\n\tif actionEnd == -1 {\n\t\treturn \"\", nil, fmt.Errorf(\"%s confirm form: unterminated action attribute\", hostname) //nolint:err113\n\t}\n\n\tformAction := htmlBody[actionStart : actionStart+actionEnd]\n\n\t// Extract hidden CSRF input value. The rendered <input> has\n\t// attributes in name-type-value order so we grab the whole tag.\n\tbefore, _, ok := strings.Cut(htmlBody, `name=\"headscale_register_confirm\"`)\n\tif !ok {\n\t\treturn \"\", nil, fmt.Errorf(\"%s confirm form: no CSRF input\", hostname) //nolint:err113\n\t}\n\n\ttagStart := strings.LastIndex(before, \"<input\")\n\tif tagStart == -1 {\n\t\treturn \"\", nil, fmt.Errorf(\"%s confirm form: no input tag for CSRF\", hostname) //nolint:err113\n\t}\n\n\ttagEnd := strings.Index(htmlBody[tagStart:], \">\")\n\tif tagEnd == -1 {","sourceCodeStart":1245,"sourceCodeEnd":1281,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/integration/scenario.go#L1245-L1281","documentation":"While auto-submitting the headscale registration confirm form, the helper found an `action=\"` attribute in the rendered HTML but no closing double quote after it. This is a string-scrape parser in the integration Scenario that extracts the form POST URL from the register/confirm page; it throws when the HTML is truncated or the template emits malformed/quoted attributes.","triggerScenarios":"Calling the Scenario registration helper that fetches the confirm page and then runs this action-extraction code: `strings.Index(htmlBody, \"action=\\\"\")` matches, but `strings.Index(htmlBody[actionStart:], \"\\\"\")` returns -1. Happens when the response body is cut off mid-attribute, when the template uses single quotes after a double quote, or when HTML-escaping inserts a quote inside the action URL.","commonSituations":"The hscontrol template for the register confirm page changed attribute quoting; a reverse proxy truncates the body; the auth flow returned an error page whose first `action=\"` occurrence is inside JavaScript or a comment rather than the form.","solutions":["Re-fetch the confirm page with curl and inspect the raw HTML around `action=` to see what the server actually rendered.","If the template changed, update this parser (or the template) in integration/scenario.go so the attribute format matches.","Replace the string scrape with golang.org/x/net/html parsing so attribute quoting/escaping no longer breaks extraction.","Verify the response is actually the confirm form (status 200, expected title) before parsing it."],"exampleFix":"// before\nactionEnd := strings.Index(htmlBody[actionStart:], `\"`)\nif actionEnd == -1 {\n    return \"\", nil, fmt.Errorf(\"%s confirm form: unterminated action attribute\", hostname)\n}\n\n// after: parse the form with x/net/html instead of string indexing\ndoc, err := html.Parse(strings.NewReader(htmlBody))\nif err != nil {\n    return \"\", nil, fmt.Errorf(\"%s confirm form: unparseable HTML: %w\", hostname, err)\n}","handlingStrategy":"validation","validationCode":"// Before scraping, cheap sanity check that the body looks like the confirm form.\nif !strings.Contains(htmlBody, \"<form\") || !strings.Contains(htmlBody, \"action=\") {\n    return \"\", nil, fmt.Errorf(\"%s confirm form: response is not a form page\", hostname)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Parse confirm pages with golang.org/x/net/html instead of string indexing.","Assert on the page identity (title or form id) before extracting fields.","Keep the template and this parser in the same change set when editing the register page."],"tags":["html-parsing","integration","registration","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}