{"record":{"id":"8c6797115e987a33","repo":"tailscale/tailscale","slug":"parse-query-parameters-v","errorCode":null,"errorMessage":"parse query parameters: %v","messagePattern":"parse query parameters: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"control/controlhttp/controlhttpserver/controlhttpserver.go","lineNumber":146,"sourceCode":"\t\tSubprotocols:   []string{controlhttpcommon.UpgradeHeaderValue},\n\t\tOriginPatterns: []string{\"*\"},\n\t\t// Disable compression because we transmit Noise messages that are not\n\t\t// compressible.\n\t\t// Additionally, Safari has a broken implementation of compression\n\t\t// (see https://github.com/nhooyr/websocket/issues/218) that makes\n\t\t// enabling it actively harmful.\n\t\tCompressionMode: websocket.CompressionDisabled,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Could not accept WebSocket connection %v\", err)\n\t}\n\tif c.Subprotocol() != controlhttpcommon.UpgradeHeaderValue {\n\t\tc.Close(websocket.StatusPolicyViolation, \"client must speak the control subprotocol\")\n\t\treturn nil, fmt.Errorf(\"Unexpected subprotocol %q\", c.Subprotocol())\n\t}\n\tif err := r.ParseForm(); err != nil {\n\t\tc.Close(websocket.StatusPolicyViolation, \"Could not parse parameters\")\n\t\treturn nil, fmt.Errorf(\"parse query parameters: %v\", err)\n\t}\n\tinitB64 := r.Form.Get(controlhttpcommon.HandshakeHeaderName)\n\tif initB64 == \"\" {\n\t\tc.Close(websocket.StatusPolicyViolation, \"missing Tailscale handshake parameter\")\n\t\treturn nil, errors.New(\"no tailscale handshake parameter in HTTP request\")\n\t}\n\tinit, err := base64.StdEncoding.DecodeString(initB64)\n\tif err != nil {\n\t\tc.Close(websocket.StatusPolicyViolation, \"invalid tailscale handshake parameter\")\n\t\treturn nil, fmt.Errorf(\"decoding base64 handshake parameter: %v\", err)\n\t}\n\n\t// Do not bind the conn's lifetime to ctx: it's typically a request\n\t// context that net/http cancels once the calling handler returns, and\n\t// the conn may be served beyond that (tailscale/corp#46806). The\n\t// handshake below is still bounded by any ctx deadline, which\n\t// controlbase.Server applies to the conn directly.\n\tconn := wsconn.NetConn(context.WithoutCancel(ctx), c, websocket.MessageBinary, r.RemoteAddr)","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/tailscale/tailscale/blob/0fd2f14deb478d09c95f38af2b0d35b492b63520/control/controlhttp/controlhttpserver/controlhttpserver.go#L128-L164","documentation":"Server-side: r.ParseForm failed while extracting the handshake parameter from the WebSocket request's query string. The query component is malformed (invalid percent-escapes, stray characters), which net/url rejects. Essentially only hostile or badly broken clients produce this.","triggerScenarios":"Query strings containing invalid escapes like '%zz', control characters, or truncated URLs; fuzzers and scanners sending malformed URLs to the control endpoint.","commonSituations":"Hand-built URL strings without proper encoding, fuzz testing of the websocket endpoint, upstream bugs concatenating query params unsafely.","solutions":["Build query strings with url.Values.Encode() instead of string concatenation","Log the raw query on failure to identify the offending client","Treat as a bad request: the socket is already closed with 1008 by the server"],"exampleFix":"// before\nu := fmt.Sprintf(\"wss://ctrl/ts2021?X-Tailscale-Handshake=%s&ts=%d\", initB64, time.Now().Unix())\n\n// after\nq := url.Values{}\nq.Set(controlhttpcommon.HandshakeHeaderName, initB64)\nu := \"wss://ctrl/ts2021?\" + q.Encode()","handlingStrategy":"try-catch","validationCode":"// Client-side: build the query with url.Values so it always parses server-side\nq := url.Values{controlhttpcommon.HandshakeHeaderName: []string{initB64}}\nu := endpoint + \"?\" + q.Encode()","typeGuard":null,"tryCatchPattern":"if err := r.ParseForm(); err != nil {\n    // Socket already closed with 1008; just log and drop the request\n    log.Printf(\"malformed query from %s: %v\", r.RemoteAddr, err)\n    return\n}","preventionTips":["Never concatenate query strings by hand; use net/url builders","Log raw queries on parse failure to identify hostile clients"],"tags":["server","websocket","query-string","url-parsing","bad-request"],"backgroundTag":"query-string-parse-failed","analyzedSha":"0fd2f14deb478d09c95f38af2b0d35b492b63520","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}