{"record":{"id":"3406d0de3260b6be","repo":"golang/go","slug":"tls-client-used-the-legacy-version-field-to-negot","errorCode":null,"errorMessage":"tls: client used the legacy version field to negotiate TLS 1.3","messagePattern":"tls: client used the legacy version field to negotiate TLS 1\\.3","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/crypto/tls/handshake_server_tls13.go","lineNumber":119,"sourceCode":"\n\tc.isHandshakeComplete.Store(true)\n\n\treturn nil\n}\n\nfunc (hs *serverHandshakeStateTLS13) processClientHello() error {\n\tc := hs.c\n\n\ths.hello = new(serverHelloMsg)\n\n\t// TLS 1.3 froze the ServerHello.legacy_version field, and uses\n\t// supported_versions instead. See RFC 8446, sections 4.1.3 and 4.2.1.\n\ths.hello.vers = VersionTLS12\n\ths.hello.supportedVersion = c.vers\n\n\tif len(hs.clientHello.supportedVersions) == 0 {\n\t\tc.sendAlert(alertIllegalParameter)\n\t\treturn errors.New(\"tls: client used the legacy version field to negotiate TLS 1.3\")\n\t}\n\n\t// Abort if the client is doing a fallback and landing lower than what we\n\t// support. See RFC 7507, which however does not specify the interaction\n\t// with supported_versions. The only difference is that with\n\t// supported_versions a client has a chance to attempt a [TLS 1.2, TLS 1.4]\n\t// handshake in case TLS 1.3 is broken but 1.2 is not. Alas, in that case,\n\t// it will have to drop the TLS_FALLBACK_SCSV protection if it falls back to\n\t// TLS 1.2, because a TLS 1.3 server would abort here. The situation before\n\t// supported_versions was not better because there was just no way to do a\n\t// TLS 1.4 handshake without risking the server selecting TLS 1.3.\n\tfor _, id := range hs.clientHello.cipherSuites {\n\t\tif id == TLS_FALLBACK_SCSV {\n\t\t\t// Use c.vers instead of max(supported_versions) because an attacker\n\t\t\t// could defeat this by adding an arbitrary high version otherwise.\n\t\t\tif c.vers < c.config.maxSupportedVersion(roleServer, c.quic != nil) {\n\t\t\t\tc.sendAlert(alertInappropriateFallback)\n\t\t\t\treturn errors.New(\"tls: client using inappropriate protocol fallback\")","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/crypto/tls/handshake_server_tls13.go#L101-L137","documentation":"RFC 8446 §4.2.1 requires a TLS 1.3 client to advertise its supported versions via the supported_versions extension, NOT the legacy ClientHello.version field (which must stay 0x0303). This error fires in processClientHello when hs.clientHello.supportedVersions is empty — the client tried to negotiate TLS 1.3 through the legacy field. The server sends an illegal_parameter alert and aborts.","triggerScenarios":"A ClientHello arrives with an empty supported_versions list. This happens when a hand-rolled or non-compliant TLS client sets legacy_version directly to 0x0304 and omits the supported_versions extension, or when an old TLS stack predating RFC 8446 is used.","commonSituations":"Custom/in-house TLS client implementations, security fuzzers generating malformed ClientHellos, old TLS libraries that predate RFC 8446, or middleboxes/proxies that rewrite and strip the ClientHello.","solutions":["Ensure the client sends the supported_versions extension listing 0x0304 (TLS 1.3)","Keep ClientHello.legacy_version pinned at 0x0303 (TLS 1.2) as mandated by RFC 8446 §4.1.2","Replace a hand-rolled TLS client with an RFC 8446-compliant library (Go's crypto/tls, current OpenSSL/BoringSSL)"],"exampleFix":"// before (non-compliant client)\nhello.version = 0x0304 // TLS 1.3 via legacy field\n// supported_versions omitted\n\n// after (compliant)\nhello.version = 0x0303\nhello.supportedVersions = []uint16{0x0304, 0x0303}","handlingStrategy":"try-catch","validationCode":"// Server-side: you cannot pre-validate a remote ClientHello.\n// Ensure YOUR server config does not force TLS 1.3-only in a way that\n// rejects compliant clients. Default config is fine.\ncfg := &tls.Config{ /* leave MinVersion/MaxVersion at defaults */ }","typeGuard":null,"tryCatchPattern":"ln, err := listener.Accept()\nif err != nil { log.Printf(\"accept: %v\", err); continue }\ngo func(c net.Conn) {\n    tlsConn := tls.Server(c, cfg)\n    if err := tlsConn.Handshake(); err != nil {\n        if strings.Contains(err.Error(), \"legacy version field\") {\n            log.Printf(\"non-compliant client (no supported_versions): %v\", err)\n        }\n        c.Close()\n        return\n    }\n    handle(tlsConn)\n}(ln)","preventionTips":["Log handshake errors with the remote address to spot non-compliant or scanning clients","Do not hand-roll TLS clients; use crypto/tls or a vetted library that always sends supported_versions","If you operate a proxy, ensure it does not strip the supported_versions extension"],"tags":["tls","go","handshake","client-hello","rfc8446","compliance","supported-versions"],"backgroundTag":null,"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}