Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: github.com/modelcontextprotocol/go-sdk
Language: Go
File: mcp/streamable.go
Function: ServeHTTP

Call path: ServeHTTP (mcp/streamable.go) → http.LocalAddrContextKey (net/http) → util.IsLoopback (internal/util/net.go)

### Primary Function

```go
func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// DNS rebinding protection: auto-enabled for localhost servers.
	// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
	if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" {
		if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil {
			if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) {
				http.Error(w, fmt.Sprintf("Forbidden: invalid Host header %q", req.Host), http.StatusForbidden)
				return
			}
		}
	}

	// Allow multiple 'Accept' headers.
	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax
	accept := strings.Split(strings.Join(req.Header.Values("Accept"), ","), ",")
	var jsonOK, streamOK bool
	for _, c := range accept {
		switch strings.TrimSpace(c) {
		case "application/json", "application/*":
			jsonOK = true
		case "text/event-stream", "text/*":
			streamOK = true
		case "*/*":
			jsonOK = true
			streamOK = true
		}
	}

	if req.Method == http.MethodGet {
		if !streamOK {
			http.Error(w, "Accept must contain 'text/event-stream' for GET requests", http.StatusBadRequest)
			return
		}
	} else if (!jsonOK || !streamOK) && req.Method != http.MethodDelete { // TODO: consolidate with handling of http method below.
		http.Error(w, "Accept must contain both 'application/json' and 'text/event-stream'", http.StatusBadRequest)
		return
	}

	sessionID := req.Header.Get(sessionIDHeader)
	var sessInfo *sessionInfo
	if sessionID != "" {
		h.mu.Lock()
		sessInfo = h.sessions[sessionID]
		h.mu.Unlock()
		if sessInfo == nil && !h.opts.Stateless {
			// Unless we're in 'stateless' mode, which doesn't perform any Session-ID
			// validation, we require that the session ID matches a known session.
			//
			// In stateless mode, a temporary transport is be created below.
			http.Error(w, "session not found", http.StatusNotFound)
			return
		}
		// Prevent session hijacking: if the session was created with a user ID,
		// verify that subsequent requests come from the same user.
		if sessInfo != nil && sessInfo.userID != "" {
			tokenInfo := auth.TokenInfoFromContext(req.Context())
			if tokenInfo == nil || tokenInfo.UserID != sessInfo.userID {
				http.Error(w, "session user mismatch", http.StatusForbidden)
				return
			}
		}
	}

	if req.Method == http.MethodDelete {
		if sessionID == "" {
			http.Error(w, "Bad Request: DELETE requires an Mcp-Session-Id header", http.StatusBadRequest)
			return
		}
		if sessInfo != nil { // sessInfo may be nil in stateless mode
			// Closing the session also removes it from h.sessions, due to the
			// onClose callback.
			sessInfo.session.Close()
		}
		w.WriteHeader(http.StatusNoContent)
		return
	}

	switch req.Method {
	case http.MethodPost, http.MethodGet:
		if req.Method == http.MethodGet && (h.opts.Stateless || sessionID == "") {
			if h.opts.Stateless {
				// Per MCP spec: server MUST return 405 if it doesn't offer SSE stream.
				// In stateless mode, GET (SSE streaming) is not supported.
				// RFC 9110 §15.5.6: 405 responses MUST include Allow header.
				w.Header().Set("Allow", "POST")
				http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
			} else {
				// In stateful mode, GET is supported but requires a session ID.
				// This is a precondition error, similar to DELETE without session.
				http.Error(w, "Bad Request: GET requires an Mcp-Session-Id header", http.StatusBadRequest)
			}
			return
		}
	default:
		// RFC 9110 §15.5.6: 405 responses MUST include Allow header.
		if h.opts.Stateless {
			w.Header().Set("Allow", "POST")
		} else {
			w.Header().Set("Allow", "GET, POST, DELETE")
		}
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	// [§2.7] of the spec (2025-06-18) states:
	//
	// "If using HTTP, the client MUST include the MCP-Protocol-Version:
	// <protocol-version> HTTP header on all subsequent requests to the MCP
	// server, allowing the MCP server to respond based on the MCP protocol
	// version.
	//
	// For example: MCP-Protocol-Version: 2025-06-18
	// The protocol version sent by the client SHOULD be the one negotiated during
	// initialization.
	//
	// For backwards compatibility, if the server does not receive an
	// MCP-Protocol-Version header, and has no other way to identify the version -
	// for example, by relying on the protocol version negotiated during
	// initialization - the server SHOULD assume protocol version 2025-03-26.
	//
	// If the server receives a request with an invalid or unsupported
	// MCP-Protocol-Version, it MUST respond with 400 Bad Request."
	//
	// Since this wasn't present in the 2025-03-26 version of the spec, this
	// effectively means:
	//  1. IF the client provides a version header, it must be a supported
	//     version.
	//  2. In stateless mode, where we've lost the state of the initialize
	//     request, we assume that whatever the client tells us is the truth (or
	//     assume 2025-03-26 if the client doesn't say anything).
	//
	// This logic matches the typescript SDK.
	//
	// [§2.7]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header
	protocolVersion := req.Header.Get(protocolVersionHeader)
	if protocolVersion == "" {
		protocolVersion = protocolVersion20250326
	}
	if !slices.Contains(supportedProtocolVersions, protocolVersion) {
		http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(supportedProtocolVersions, ",")), http.StatusBadRequest)
		return
	}

	if sessInfo == nil {
		server := h.getServer(req)
		if server == nil {
			// The getServer argument to NewStreamableHTTPHandler returned nil.
			http.Error(w, "no server available", http.StatusBadRequest)
			return
		}
		if sessionID == "" {
			// In stateless mode, sessionID may be nonempty even if there's no
			// existing transport.
			sessionID = server.opts.GetSessionID()
		}
		transport := &StreamableServerTransport{
			SessionID:    sessionID,
			Stateless:    h.opts.Stateless,
			EventStore:   h.opts.EventStore,
			jsonResponse: h.opts.JSONResponse,
			logger:       h.opts.Logger,
		}

		// Sessions without a session ID are also stateless: there's no way to
		// address them.
		stateless := h.opts.Stateless || sessionID == ""
		// To support stateless mode, we initialize the session with a default
		// state, so that it doesn't reject subsequent requests.
		var connectOpts *ServerSessionOptions
		if stateless {
			// Peek at the body to see if it is initialize or initialized.
			// We want those to be handled as usual.
			var hasInitialize, hasInitialized bool
			{
				// TODO: verify that this allows protocol version negotiation for
				// stateless servers.
				body, err := io.ReadAll(req.Body)
				if err != nil {
					http.Error(w, "failed to read body", http.StatusInternalServerError)
					return
				}
				req.Body.Close()

				// Reset the body so that it can be read later.
				req.Body = io.NopCloser(bytes.NewBuffer(body))

				msgs, _, err := readBatch(body)
				if err == nil {
					for _, msg := range msgs {
						if req, ok := msg.(*jsonrpc.Request); ok {
							switch req.Method {
							case methodInitialize:
								hasInitialize = true
							case notificationInitialized:
								hasInitialized = true
							}
						}
					}
				}
			}

			// If we don't have InitializeParams or InitializedParams in the request,
			// set the initial state to a default value.
			state := new(ServerSessionState)
			if !hasInitialize {
				state.InitializeParams = &InitializeParams{
					ProtocolVersion: protocolVersion,
				}
			}
			if !hasInitialized {
				state.InitializedParams = new(InitializedParams)
			}
			state.LogLevel = "info"
			connectOpts = &ServerSessionOptions{
				State: state,
			}
		} else {
			// Cleanup is only required in stateful mode, as transportation is
			// not stored in the map otherwise.
			connectOpts = &ServerSessionOptions{
				onClose: func() {
					h.mu.Lock()
					defer h.mu.Unlock()
					if info, ok := h.sessions[transport.SessionID]; ok {
						info.stopTimer()
						delete(h.sessions, transport.SessionID)
						if h.onTransportDeletion != nil {
							h.onTransportDeletion(transport.SessionID)
						}
					}
				},
			}
		}

		// Pass req.Context() here, to allow middleware to add context values.
		// The context is detached in the jsonrpc2 library when handling the
		// long-running stream.
		session, err := server.Connect(req.Context(), transport, connectOpts)
		if err != nil {
			http.Error(w, "failed connection", http.StatusInternalServerError)
			return
		}
		// Capture the user ID from the token info to enable session hijacking
		// prevention on subsequent requests.
		var userID string
		if tokenInfo := auth.TokenInfoFromContext(req.Context()); tokenInfo != nil {
			userID = tokenInfo.UserID
		}
		sessInfo = &sessionInfo{
			session:   session,
			transport: transport,
			userID:    userID,
		}

		if stateless {
			// Stateless mode: close the session when the request exits.
			defer session.Close() // close the fake session after handling the request
		} else {
			// Otherwise, save the transport so that it can be reused

			// Clean up the session when it times out.
			//
			// Note that the timer here may fire multiple times, but
			// sessInfo.session.Close is idempotent.
			if h.opts.SessionTimeout > 0 {
				sessInfo.timeout = h.opts.SessionTimeout
				sessInfo.timer = time.AfterFunc(sessInfo.timeout, func() {
					sessInfo.session.Close()
				})
			}
			h.mu.Lock()
			h.sessions[transport.SessionID] = sessInfo
			h.mu.Unlock()
			defer func() {
				// If initialization failed, clean up the session (#578).
				if session.InitializeParams() == nil {
					// Initialization failed.
					session.Close()
				}
			}()
		}
	}

	if req.Method == http.MethodPost {
		sessInfo.startPOST()
		defer sessInfo.endPOST()
	}

	sessInfo.transport.ServeHTTP(w, req)
}
```

### Cross-File Context

[IsLoopback — function — internal/util/net.go:1-26]
```go
func IsLoopback(addr string) bool {
	host, _, err := net.SplitHostPort(addr)
	if err != nil {
		// If SplitHostPort fails, it might be just a host without a port.
		host = strings.Trim(addr, "[]")
	}
	if host == "localhost" {
		return true
	}
	ip, err := netip.ParseAddr(host)
	if err != nil {
		return false
	}
	return ip.IsLoopback()
}
```

[disablelocalhostprotection — var — mcp/streamable.go:228]
var disablelocalhostprotection = mcpgodebug.Value("disablelocalhostprotection")

[DisableLocalhostProtection — field — mcp/streamable.go:176]
DisableLocalhostProtection bool

Control flow: The ServeHTTP function processes incoming HTTP requests through a multi-stage pipeline. First, DNS rebinding protection checks if the request arrives on a loopback interface and validates that the Host header also resolves to a loopback address, returning 403 if mismatched. Next, Accept header parsing determines which content types the client supports. Method-specific validation then enforces Accept header requirements: GET requires text/event-stream, and non-GET/non-DELETE methods require both application/json and text/event-stream. Session ID handling looks up existing sessions under mutex protection and validates user ID consistency for session hijacking prevention. DELETE requests clean up sessions. POST and GET proceed to protocol version validation, followed by session creation if needed (with special stateless mode handling that peeks at the request body). Finally, the transport's ServeHTTP is called to handle the request. Early returns occur on 403 Forbidden, 400 Bad Request, 404 Not Found, and 405 Method Not Allowed responses throughout the pipeline.

Data flow: The local address is extracted from the request context via req.Context().Value(http.LocalAddrContextKey) as a net.Addr, then converted to string via localAddr.String() and passed to util.IsLoopback(). The Host header value from req.Host is passed directly to util.IsLoopback(). Both results feed into the DNS rebinding guard condition. The Accept header values are parsed by splitting on commas and trimming whitespace, setting jsonOK and streamOK boolean flags. Session ID flows from req.Header.Get(sessionIDHeader) through mutex-protected map lookup in h.sessions. The token info flows from auth.TokenInfoFromContext(req.Context()) and is compared against sessInfo.userID for session hijacking prevention. Protocol version flows from req.Header.Get(protocolVersionHeader) with a default fallback, validated against supportedProtocolVersions via slices.Contains.

Verdict: Not Vulnerable

Reviewed region:
[mcp/streamable.go]
```go
func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// DNS rebinding protection: auto-enabled for localhost servers.
	// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
	if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" {
		if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil {
			if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) {
				http.Error(w, fmt.Sprintf("Forbidden: invalid Host header %q", req.Host), http.StatusForbidden)
				return
			}
		}
	}

	// Allow multiple 'Accept' headers.
	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax
	accept := strings.Split(strings.Join(req.Header.Values("Accept"), ","), ",")
	var jsonOK, streamOK bool
	for _, c := range accept {
		switch strings.TrimSpace(c) {
		case "application/json", "application/*":
			jsonOK = true
		case "text/event-stream", "text/*":
			streamOK = true
		case "*/*":
			jsonOK = true
			streamOK = true
		}
	}

	if req.Method == http.MethodGet {
		if !streamOK {
			http.Error(w, "Accept must contain 'text/event-stream' for GET requests", http.StatusBadRequest)
			return
		}
	} else if (!jsonOK || !streamOK) && req.Method != http.MethodDelete { // TODO: consolidate with handling of http method below.
		http.Error(w, "Accept must contain both 'application/json' and 'text/event-stream'", http.StatusBadRequest)
		return
	}

	sessionID := req.Header.Get(sessionIDHeader)
	var sessInfo *sessionInfo
	if sessionID != "" {
		h.mu.Lock()
		sessInfo = h.sessions[sessionID]
		h.mu.Unlock()
		if sessInfo == nil && !h.opts.Stateless {
			// Unless we're in 'stateless' mode, which doesn't perform any Session-ID
			// validation, we require that the session ID matches a known session.
			//
			// In stateless mode, a temporary transport is be created below.
			http.Error(w, "session not found", http.StatusNotFound)
			return
		}
		// Prevent session hijacking: if the session was created with a user ID,
		// verify that subsequent requests come from the same user.
		if sessInfo != nil && sessInfo.userID != "" {
			tokenInfo := auth.TokenInfoFromContext(req.Context())
			if tokenInfo == nil || tokenInfo.UserID != sessInfo.userID {
				http.Error(w, "session user mismatch", http.StatusForbidden)
				return
			}
		}
	}

	if req.Method == http.MethodDelete {
		if sessionID == "" {
			http.Error(w, "Bad Request: DELETE requires an Mcp-Session-Id header", http.StatusBadRequest)
			return
		}
		if sessInfo != nil { // sessInfo may be nil in stateless mode
			// Closing the session also removes it from h.sessions, due to the
			// onClose callback.
			sessInfo.session.Close()
		}
		w.WriteHeader(http.StatusNoContent)
		return
	}

	switch req.Method {
	case http.MethodPost, http.MethodGet:
		if req.Method == http.MethodGet && (h.opts.Stateless || sessionID == "") {
			if h.opts.Stateless {
				// Per MCP spec: server MUST return 405 if it doesn't offer SSE stream.
				// In stateless mode, GET (SSE streaming) is not supported.
				// RFC 9110 §15.5.6: 405 responses MUST include Allow header.
				w.Header().Set("Allow", "POST")
				http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
			} else {
				// In stateful mode, GET is supported but requires a session ID.
				// This is a precondition error, similar to DELETE without session.
				http.Error(w, "Bad Request: GET requires an Mcp-Session-Id header", http.StatusBadRequest)
			}
			return
		}
	default:
		// RFC 9110 §15.5.6: 405 responses MUST include Allow header.
		if h.opts.Stateless {
			w.Header().Set("Allow", "POST")
		} else {
			w.Header().Set("Allow", "GET, POST, DELETE")
		}
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	// [§2.7] of the spec (2025-06-18) states:
	//
	// "If using HTTP, the client MUST include the MCP-Protocol-Version:
	// <protocol-version> HTTP header on all subsequent requests to the MCP
	// server, allowing the MCP server to respond based on the MCP protocol
	// version.
	//
	// For example: MCP-Protocol-Version: 2025-06-18
	// The protocol version sent by the client SHOULD be the one negotiated during
	// initialization.
	//
	// For backwards compatibility, if the server does not receive an
	// MCP-Protocol-Version header, and has no other way to identify the version -
	// for example, by relying on the protocol version negotiated during
	// initialization - the server SHOULD assume protocol version 2025-03-26.
	//
	// If the server receives a request with an invalid or unsupported
	// MCP-Protocol-Version, it MUST respond with 400 Bad Request."
	//
	// Since this wasn't present in the 2025-03-26 version of the spec, this
	// effectively means:
	//  1. IF the client provides a version header, it must be a supported
	//     version.
	//  2. In stateless mode, where we've lost the state of the initialize
	//     request, we assume that whatever the client tells us is the truth (or
	//     assume 2025-03-26 if the client doesn't say anything).
	//
	// This logic matches the typescript SDK.
	//
	// [§2.7]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header
	protocolVersion := req.Header.Get(protocolVersionHeader)
	if protocolVersion == "" {
		protocolVersion = protocolVersion20250326
	}
	if !slices.Contains(supportedProtocolVersions, protocolVersion) {
		http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(supportedProtocolVersions, ",")), http.StatusBadRequest)
		return
	}

	if sessInfo == nil {
		server := h.getServer(req)
		if server == nil {
			// The getServer argument to NewStreamableHTTPHandler returned nil.
			http.Error(w, "no server available", http.StatusBadRequest)
			return
		}
		if sessionID == "" {
			// In stateless mode, sessionID may be nonempty even if there's no
			// existing transport.
			sessionID = server.opts.GetSessionID()
		}
		transport := &StreamableServerTransport{
			SessionID:    sessionID,
			Stateless:    h.opts.Stateless,
			EventStore:   h.opts.EventStore,
			jsonResponse: h.opts.JSONResponse,
			logger:       h.opts.Logger,
		}

		// Sessions without a session ID are also stateless: there's no way to
		// address them.
		stateless := h.opts.Stateless || sessionID == ""
		// To support stateless mode, we initialize the session with a default
		// state, so that it doesn't reject subsequent requests.
		var connectOpts *ServerSessionOptions
		if stateless {
			// Peek at the body to see if it is initialize or initialized.
			// We want those to be handled as usual.
			var hasInitialize, hasInitialized bool
			{
				// TODO: verify that this allows protocol version negotiation for
				// stateless servers.
				body, err := io.ReadAll(req.Body)
				if err != nil {
					http.Error(w, "failed to read body", http.StatusInternalServerError)
					return
				}
				req.Body.Close()

				// Reset the body so that it can be read later.
				req.Body = io.NopCloser(bytes.NewBuffer(body))

				msgs, _, err := readBatch(body)
				if err == nil {
					for _, msg := range msgs {
						if req, ok := msg.(*jsonrpc.Request); ok {
							switch req.Method {
							case methodInitialize:
								hasInitialize = true
							case notificationInitialized:
								hasInitialized = true
							}
						}
					}
				}
			}

			// If we don't have InitializeParams or InitializedParams in the request,
			// set the initial state to a default value.
			state := new(ServerSessionState)
			if !hasInitialize {
				state.InitializeParams = &InitializeParams{
					ProtocolVersion: protocolVersion,
				}
			}
			if !hasInitialized {
				state.InitializedParams = new(InitializedParams)
			}
			state.LogLevel = "info"
			connectOpts = &ServerSessionOptions{
				State: state,
			}
		} else {
			// Cleanup is only required in stateful mode, as transportation is
			// not stored in the map otherwise.
			connectOpts = &ServerSessionOptions{
				onClose: func() {
					h.mu.Lock()
					defer h.mu.Unlock()
					if info, ok := h.sessions[transport.SessionID]; ok {
						info.stopTimer()
						delete(h.sessions, transport.SessionID)
						if h.onTransportDeletion != nil {
							h.onTransportDeletion(transport.SessionID)
						}
					}
				},
			}
		}

		// Pass req.Context() here, to allow middleware to add context values.
		// The context is detached in the jsonrpc2 library when handling the
		// long-running stream.
		session, err := server.Connect(req.Context(), transport, connectOpts)
		if err != nil {
			http.Error(w, "failed connection", http.StatusInternalServerError)
			return
		}
		// Capture the user ID from the token info to enable session hijacking
		// prevention on subsequent requests.
		var userID string
		if tokenInfo := auth.TokenInfoFromContext(req.Context()); tokenInfo != nil {
			userID = tokenInfo.UserID
		}
		sessInfo = &sessionInfo{
			session:   session,
			transport: transport,
			userID:    userID,
		}

		if stateless {
			// Stateless mode: close the session when the request exits.
			defer session.Close() // close the fake session after handling the request
		} else {
			// Otherwise, save the transport so that it can be reused

			// Clean up the session when it times out.
			//
			// Note that the timer here may fire multiple times, but
			// sessInfo.session.Close is idempotent.
			if h.opts.SessionTimeout > 0 {
				sessInfo.timeout = h.opts.SessionTimeout
				sessInfo.timer = time.AfterFunc(sessInfo.timeout, func() {
					sessInfo.session.Close()
				})
			}
			h.mu.Lock()
			h.sessions[transport.SessionID] = sessInfo
			h.mu.Unlock()
			defer func() {
				// If initialization failed, clean up the session (#578).
				if session.InitializeParams() == nil {
					// Initialization failed.
					session.Close()
				}
			}()
		}
	}

	if req.Method == http.MethodPost {
		sessInfo.startPOST()
		defer sessInfo.endPOST()
	}

	sessInfo.transport.ServeHTTP(w, req)
}
```

Why it is not vulnerable: The DNS rebinding protection is correctly implemented. When the local address resolves to a loopback interface (util.IsLoopback(localAddr.String()) returns true), the code enforces that the Host header must also resolve to a loopback address (util.IsLoopback(req.Host) must return true). If the Host header does not resolve to loopback, the request is rejected with a 403 Forbidden response. The util.IsLoopback function properly handles multiple address formats: it uses net.SplitHostPort to extract the host component (handling addresses with ports), trims IPv6 brackets, checks for the 'localhost' literal, and uses netip.ParseAddr with ip.IsLoopback() for IP address validation. The protection is properly gated behind DisableLocalhostProtection and disablelocalhostprotection configuration flags, allowing administrators to explicitly disable it when needed (e.g., behind a reverse proxy). Invalid or unparseable addresses in util.IsLoopback return false, which is a safe default.

Security invariants:
- When localhost protection is not explicitly disabled (via h.opts.DisableLocalhostProtection or disablelocalhostprotection), the DNS rebinding guard must execute. Enforced by: if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1"
- The local address must be successfully extracted from the request context and must be non-nil before applying the loopback check. Enforced by: if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil
- When the server is bound to a loopback interface, the Host header must also resolve to a loopback address. Enforced by: if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) { return 403 }
- util.IsLoopback must correctly parse addresses with and without port numbers. Enforced by: net.SplitHostPort(addr) to extract host, with fallback handling when it fails
- util.IsLoopback must correctly identify loopback addresses in multiple formats: 'localhost' literal, IPv4 loopback (127.0.0.0/8), and IPv6 loopback (::1). Enforced by: host == "localhost" check and ip.IsLoopback() after netip.ParseAddr
- Invalid or unparseable addresses must not be treated as loopback (safe default). Enforced by: if err != nil { return false } in util.IsLoopback
- Session access is properly synchronized to prevent race conditions. Enforced by: h.mu.Lock()/h.mu.Unlock() around h.sessions map access
- Session hijacking is prevented by verifying user ID consistency. Enforced by: if tokenInfo == nil || tokenInfo.UserID != sessInfo.userID { return 403 }

Invariant verification:
- DNS rebinding protection is enabled by default and can only be explicitly disabled: holds=true. Evidence: The condition !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" requires both the config field and the debug variable to explicitly disable the protection
- Local address extraction is safe and guards against missing context value: holds=true. Evidence: The type assertion with ok check: req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil ensures the code only proceeds when a valid address is present
- Loopback detection correctly handles host:port format addresses: holds=true. Evidence: util.IsLoopback calls net.SplitHostPort(addr) which correctly separates host and port components, then operates on the host portion only
- Loopback detection correctly identifies localhost, IPv4 loopback, and IPv6 loopback: holds=true. Evidence: util.IsLoopback checks host == "localhost" for the literal, and uses netip.ParseAddr(host) followed by ip.IsLoopback() which covers 127.0.0.0/8 and ::1
- Unparseable addresses default to non-loopback (fail-safe): holds=true. Evidence: Both error paths in util.IsLoopback (net.SplitHostPort error and netip.ParseAddr error) lead to return false or fall through to return false
- Mismatched Host header on loopback interface results in request rejection: holds=true. Evidence: if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) { http.Error(w, ..., http.StatusForbidden); return } rejects requests where local address is loopback but Host is not
- Session map access is protected by mutex: holds=true. Evidence: h.mu.Lock() before sessInfo = h.sessions[sessionID] and h.mu.Unlock() after; same pattern for write operations with h.sessions[transport.SessionID] = sessInfo

Residual risk notes: The DNS rebinding protection is skipped if the local address cannot be extracted from the request context (e.g., if the HTTP server does not populate http.LocalAddrContextKey). This is acceptable because the protection specifically applies to servers bound to loopback interfaces; if no local address information is available, the condition cannot be evaluated. The util.IsLoopback function does not perform DNS resolution on hostnames in the Host header, but this is correct behavior since the check is designed to detect when the Host header contains a non-loopback address literal or hostname that was not validated as loopback during the connection setup. The req.Host field is controlled by the client, but the IsLoopback check on it is the appropriate defense mechanism.

## Analysis

### Purpose of the check

The first block in `ServeHTTP` is a DNS-rebinding protection mechanism. The intent: if the server is bound to a loopback address (localhost), reject requests whose `Host` header doesn't also reference loopback. This stops attackers from using DNS rebinding to make a victim browser send requests to `127.0.0.1:xxxx` while presenting an attacker-controlled hostname in `Host`.

### Control & Data Flow

1. `localAddr` comes from `req.Context().Value(http.LocalAddrContextKey)`, which is set by Go's `net/http` server to the actual local listening address (trusted, not attacker-controlled).
2. `localAddr.String()` typically returns something like `127.0.0.1:8080` or `[::1]:8080`.
3. `req.Host` is attacker-controlled (HTTP header).
4. Both are passed to `util.IsLoopback`.

### IsLoopback semantics

```go
host, _, err := net.SplitHostPort(addr)
if err != nil {
    host = strings.Trim(addr, "[]")
}
if host == "localhost" { return true }
ip, err := netip.ParseAddr(host)
if err != nil { return false }
return ip.IsLoopback()
```

For `req.Host`:
- A bare hostname like `evil.com` → `SplitHostPort` fails (no port) → host becomes `evil.com` → not "localhost", not parseable as IP → returns false. ✓
- `localhost:8080` → splits to `localhost` → returns true. ✓
- `127.0.0.1` → host is `127.0.0.1` → IP, IsLoopback → true. ✓
- `127.0.0.1:8080` → splits cleanly → true. ✓
- `[::1]:8080` → splits to `::1` → IP, IsLoopback → true. ✓

So the check correctly classifies common loopback host headers as loopback and non-loopback ones as non-loopback.

### Potential weak spots

1. **Only enabled when local address is loopback.** If the server is also bound to non-loopback interfaces, the check is bypassed because `IsLoopback(localAddr)` is false. That's the documented behavior ("auto-enabled for localhost servers"). Operators who run on `0.0.0.0` need to take their own DNS-rebinding measures, but since at least one connection actually arrives on a loopback interface only when the listening side is loopback, this is acceptable.

2. **IP literal in Host bypass?** A DNS rebinding attack typically requires the attacker to make the victim's browser resolve a hostname to 127.0.0.1. The `Host` header set by the browser will then be the original attacker domain (e.g., `evil.com`), not `127.0.0.1`. So this Host check works against the standard rebinding scenario. An attacker who can directly send requests with `Host: 127.0.0.1` is not a rebinding threat — they could just connect directly.

3. **`0.0.0.0` and `0:0:0:0:0:0:0:0` in Host header?** `netip.ParseAddr("0.0.0.0").IsLoopback()` returns false. Not a bypass.

4. **Override mechanism.** Two ways to disable: `h.opts.DisableLocalhostProtection` (explicit opt-out) and the `disablelocalhostprotection` debug env. Both are intentional opt-outs.

5. **Override env-variable default.** If `mcpgodebug.Value("disablelocalhostprotection")` returns `"1"` from a process-wide env variable, an attacker who can set environment variables already owns the process. Not a vuln.

6. **Path-traversal-like trick on Host?** Host is parsed by Go's `http` package; trailing whitespace is stripped. No obvious way to make `IsLoopback("evil.com")` return true.

### Conclusion

The DNS rebinding mitigation is correctly implemented: it enables only when serving from loopback, it correctly identifies loopback hosts (including IPv4, IPv6, `localhost`, with and without port), and rejects non-loopback Host headers with 403. The remainder of `ServeHTTP` (Accept-header validation, session lookup, user-ID match for hijacking prevention, protocol-version handling) does not interact with the loopback check in any way that would weaken it.

I see no real vulnerability on the path `ServeHTTP → http.LocalAddrContextKey → util.IsLoopback`. The behaviour matches the stated security goal and the spec reference.

### Residual uncertainty

- The protection assumes operators using `0.0.0.0` understand the rebinding risk themselves. If a future configuration auto-binds to all interfaces while being marketed as "localhost only", this guard would silently not fire.
- The `mcpgodebug.Value` source isn't shown — if it reads, e.g., a request header or remote-influenced state instead of an env variable, that would be a problem; based on the naming convention (mirroring `godebug`) it almost certainly reads a process env, so this is a low-likelihood concern.

**Verdict: Not vulnerable on the supplied path.**

mcp/streamable.go ServeHTTP, lines covering the DNS-rebinding guard at the top of the function, plus internal/util/net.go IsLoopback (lines 1-26).

No exploit path identified. Standard DNS-rebinding scenarios result in the victim browser sending Host headers containing the attacker's hostname (e.g. evil.com), which IsLoopback correctly classifies as non-loopback, triggering 403. Direct attackers who can craft Host: 127.0.0.1 already have local network access and rebinding is not the relevant threat model.

The guard activates whenever the listening socket's local address is loopback (taken from the trusted http.LocalAddrContextKey, not attacker-controlled), and rejects any request whose Host header is not also loopback. IsLoopback handles host:port, bare host, IPv6 bracketed forms, the literal 'localhost', and falls through to netip.ParseAddr().IsLoopback() for IP literals, so common loopback representations are matched and arbitrary hostnames return false. Both opt-outs (DisableLocalhostProtection option and the mcpgodebug-style env switch) are explicit administrator choices.

localAddr originates from net/http server context and reflects the real listening socket, not request data.

If localAddr is loopback and req.Host does not resolve to a loopback host/IP, the request is rejected with 403 before any session work.

IsLoopback returns true only for 'localhost', IPv4/IPv6 loopback literals, optionally with port; all other strings (including non-loopback IPs and arbitrary hostnames) return false.

Disabling the protection requires either an explicit option or a process-level debug env, both under operator control.

Walked representative Host header values (evil.com, evil.com:80, 127.0.0.1, 127.0.0.1:8080, [::1]:8080, localhost, localhost:8080, 0.0.0.0) through IsLoopback and confirmed correct classification. Confirmed localAddr is provided by net/http and not derivable from request headers. Confirmed neither override is influenced by request data.

If a server binds to 0.0.0.0 (or any non-loopback interface) the auto-protection does not engage, leaving DNS-rebinding mitigation to the operator. mcpgodebug.Value is not shown; assumed to read process env consistent with its name—if it ever sourced from request-controlled input, that would weaken the guard.

ServeHTTP (mcp/streamable.go) → req.Context().Value(http.LocalAddrContextKey) → util.IsLoopback(localAddr.String()) and util.IsLoopback(req.Host) → 403 on mismatch


System:
You are a senior application security auditor reviewing code for potential vulnerabilities.

You will be given a candidate function, repository and file metadata, a reduced call path, and resolved cross-file context such as helper functions, structs, macros, constants, and related definitions. Use the supplied context actively, but stay strictly grounded in what is actually shown.

Work from the supplied code and surrounding context as the primary evidence. Do not assume a bug exists just because the sample was retrieved.

Analyze both control flow and data flow. Trace the relevant execution paths and track security-relevant data through inputs, transformations, conditions, helper calls, outputs, and side effects.

Reason about attacker influence, identify relevant guards or missing guards, and explain exploitability or non-exploitability before reaching a verdict.

When the code is vulnerable, explain the vulnerable behavior, root cause, realistic attack preconditions, likely impact, and the most precise vulnerability classification justified by the supplied code.

When the code is not vulnerable, explain the concrete invariants and guards that prevent the suspected issue, and note any residual uncertainty that remains from the supplied context.

Keep the analysis concrete and code-grounded. Separate observed evidence from inferred conclusion.

Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: github.com/chamilo/chamilo-lms
Language: PHP
File: main/document/document.php
Function: document_listing_display

Call path: document.php (main/document/document.php) → DocumentManager::get_document_data_by_id (main/document/document.php) → document_listing_display (main/document/document.php)

### Primary Function

```php
<?php
// Document title with link and comment
$titleWithLink = $link.$session_img.'<br />'.$invisibility_span_open;
$commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset));
if (!empty($commentText)) {
    $titleWithLink .= '<em>'.$commentText.'</em>';
}
$titleWithLink .= $invisibility_span_close.$user_link;
$row[] = $titleWithLink;
```

### Cross-File Context

[Security::remove_XSS — method — main/inc/lib/security.lib.php:203]
public static function remove_XSS($var, $user_status = null, $filter_terms = false) { if ($filter_terms) { $var = self::filter_terms($var); } if (empty($user_status)) { if (api_is_anonymous()) { $user_status = ANONYMOUS; } else { if (api_is_allowed_to_edit()) { $user_status = COURSEMANAGER; } else { $user_status = STUDENT; } } } if ($user_status == COURSEMANAGERLOWSECURITY) { return $var; } static $purifier = []; if (!isset($purifier[$user_status])) { $cache_dir = api_get_path(SYS_ARCHIVE_PATH).'Serializer'; if (!file_exists($cache_dir)) { $mode = api_get_permissions_for_new_directories(); mkdir($cache_dir, $mode); } $config = HTMLPurifier_Config::createDefault(); $config->set('Cache.SerializerPath', $cache_dir); $config->set('Core.Encoding', api_get_system_encoding()); $config->set('HTML.Doctype', 'XHTML 1.0 Transitional'); $config->set('HTML.MaxImgLength', '2560'); $config->set('HTML.TidyLevel', 'light'); $config->set('Core.ConvertDocumentToFragment', false); $config->set('Core.RemoveProcessingInstructions', true); if (api_get_setting('enable_iframe_inclusion') == 'true') { $config->set('Filter.Custom', [new AllowIframes()]); } $config->set('Attr.AllowedFrameTargets', ['_blank', '_top', '_self', '_parent']); if ($user_status == STUDENT) { global $allowed_html_student; $config->set('HTML.SafeEmbed', true); $config->set('HTML.SafeObject', true); $config->set('Filter.YouTube', true); $config->set('HTML.FlashAllowFullScreen', true); $config->set('HTML.Allowed', $allowed_html_student); } elseif ($user_status == COURSEMANAGER) { global $allowed_html_teacher; $config->set('HTML.SafeEmbed', true); $config->set('HTML.SafeObject', true); $config->set('Filter.YouTube', true); $config->set('HTML.FlashAllowFullScreen', true); $config->set('HTML.Allowed', $allowed_html_teacher); } else { global $allowed_html_anonymous; $config->set('HTML.Allowed', $allowed_html_anonymous); } $config->set('Attr.EnableID', true); $config->set('CSS.AllowImportant', true); $config->set('CSS.AllowTricky', true); $config->set('CSS.Proprietary', true); $config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true, 'ftp' => true, 'nntp' => true, 'news' => true, 'data' => true]); $config->set('HTML.SafeIframe', true); $config->set('HTML.DefinitionID', 'html5-definitions'); $config->set('HTML.DefinitionRev', 1); if ($def = $config->maybeGetRawHTMLDefinition()) { $def->addElement('video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', ['src' => 'URI', 'type' => 'Text', 'width' => 'Length', 'height' => 'Length', 'poster' => 'URI', 'preload' => 'Enum#auto,metadata,none', 'controls' => 'Bool']); $def->addElement('audio', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', ['autoplay' => 'Bool', 'src' => 'URI', 'loop' => 'Bool', 'preload' => 'Enum#auto,metadata,none', 'controls' => 'Bool', 'muted' => 'Bool']); $def->addElement('source', 'Block', 'Flow', 'Common', ['src' => 'URI', 'type' => 'Text']); } $purifier[$user_status] = new HTMLPurifier($config); } if (is_array($var)) { return $purifier[$user_status]->purifyArray($var); } else { return $purifier[$user_status]->purify($var); } }

Provide the final verdict as strict JSON.

Return only one JSON object and no surrounding prose.

Return one JSON object with these keys:
- vulnerable
- cwe
- severity
- reviewed_region
- vulnerable_region
- root_cause
- exploitability
- attack_preconditions
- impact
- non_vulnerable_reason
- security_invariants
- invariant_verification
- residual_risk_notes
- call_path

Control flow: User requests document listing -> document.php retrieves document data via DocumentManager -> builds HTML title with link and comment -> outputs to browser without sanitization -> XSS executes in victim's browser

Data flow: Document title (user-controlled) -> $link variable -> $titleWithLink concatenation -> $row[] array -> HTML output -> Browser execution

Verdict: Vulnerable
Vulnerable region:
[main/document/document.php]
```php
);

            // Document title with link and comment
            $titleWithLink = $link.$session_img.'<br />'.$invisibility_span_open;
            $commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset));
            if (!empty($commentText)) {
                $titleWithLink .= '<em>'.$commentText.'</em>';
            }
```
CWE: CWE-79
Severity: MEDIUM

Root cause: The $link variable containing the document title HTML is assigned directly to $titleWithLink without passing through Security::remove_XSS() sanitization. The $link variable is constructed from document data including the title which can be set/modified by users with document creation/editing permissions, allowing injection of malicious HTML/script tags that execute when the document list is displayed.

Exploitability: An attacker with course document creation/edit permissions creates or renames a document with a malicious JavaScript payload in the title. When a user views the course document listing, the JavaScript executes in their browser.

Attack preconditions:
- Attacker must have ability to create or rename documents in a course (authenticated user with document creation/edit permissions)
- Attacker crafts document title containing malicious HTML/JavaScript
- Victim user views the affected document listing page

Likely impact: An attacker with course document creation/edit permissions creates or renames a document with a malicious JavaScript payload in the title. When a user views the course document listing, the JavaScript executes in their browser.

);

            // Document title with link and comment
            $titleWithLink = $link.$session_img.'
'.$invisibility_span_open;
            $commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset));
            if (!empty($commentText)) {
                $titleWithLink .= ''.$commentText.'';
            }

## Analysis

### Code under review

```php
$titleWithLink = $link.$session_img.'<br />'.$invisibility_span_open;
$commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset));
if (!empty($commentText)) {
    $titleWithLink .= '<em>'.$commentText.'</em>';
}
$titleWithLink .= $invisibility_span_close.$user_link;
$row[] = $titleWithLink;
```

### Data Flow

The only attacker-influenced sink in this snippet is `$document_data['comment']`, which originates from `DocumentManager::get_document_data_by_id` (a document's metadata, settable by users with upload privileges).

Before being concatenated into the HTML output, it is passed through:
1. `htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset)` — encodes `<`, `>`, `"`, `'`, `&`.
2. `nl2br(...)` — converts newlines into `<br />` tags but does not introduce attacker-controlled HTML (it only inserts the literal `<br />` next to already-encoded content).

The resulting `$commentText` is wrapped in `<em>...</em>` and concatenated.

### Other variables

- `$link`, `$session_img`, `$invisibility_span_open`, `$invisibility_span_close`, `$user_link` are not shown here; they are constructed elsewhere in `document_listing_display`. These are out of scope of this snippet.
- The cross-file context provides `Security::remove_XSS`, but it is not used here. The snippet relies on `htmlspecialchars`, which is the canonical XSS-prevention primitive for text content placed into HTML body context.

### Guard / Invariant Analysis

- `htmlspecialchars(..., ENT_QUOTES, $charset)` with both quote styles encoded is the standard defense for HTML text-node and attribute contexts. The output is wrapped between `<em>` and `</em>` (text node context), where this encoding is sufficient.
- `nl2br` operates after `htmlspecialchars`, so any `<` or `>` in the original input has already been escaped; the `<br />` it inserts is a known-safe constant string.
- `$charset` must match the page encoding for `htmlspecialchars` to be safe; in Chamilo this is conventionally UTF-8 (`api_get_system_encoding()` / `$charset` global). No evidence of a mismatch in the supplied context.

### Conclusion

For the specific variable handled in this snippet (`$document_data['comment']`), encoding is properly applied before HTML concatenation. There is no XSS or injection vulnerability in the code shown.

Residual uncertainty:
- The other concatenated variables (`$link`, `$user_link`, `$invisibility_span_*`, `$session_img`) are not shown. If any of them carries unsanitized attacker-controlled data, that would be a separate issue — but it is outside the scope of this snippet and not evidenced.
- If `$charset` were ever set to something non-UTF-8-compatible, edge cases could arise; no evidence of that.

### Verdict

**Not vulnerable** based on the supplied code. The `comment` field is safely escaped via `htmlspecialchars(..., ENT_QUOTES, $charset)` before being interpolated into HTML, and `nl2br` only adds safe markup. The retrieval likely flagged this as suspicious because it does not use `Security::remove_XSS`, but `htmlspecialchars` is appropriate (and arguably stricter) for text-node output.

main/document/document.php — document_listing_display: titleWithLink construction including comment encoding

The only attacker-influenced field in the snippet, $document_data['comment'], is encoded with htmlspecialchars(..., ENT_QUOTES, $charset) before concatenation into the HTML body. nl2br is applied after encoding, so it only inserts a constant '
' between already-escaped content. As placed inside ... (HTML text-node context), this encoding is sufficient to neutralize HTML/JS injection.

An authenticated user able to set a document comment would need a sink that does not encode the value; this snippet provides encoding, so no exploitation path exists here.

None for the comment field handled by this snippet.

htmlspecialchars with ENT_QUOTES is applied to $document_data['comment'] prior to interpolation into the HTML output, which is the standard and adequate defense for text-node context. nl2br does not introduce attacker-controlled markup.

User-controlled text rendered into HTML body must be HTML-encoded.

Encoding must occur before any transformation that adds markup; here htmlspecialchars precedes nl2br.

Charset passed to htmlspecialchars matches the page encoding (Chamilo $charset).

Verified by direct inspection: $commentText = nl2br(htmlspecialchars($document_data['comment'], ENT_QUOTES, $charset)); the encoded value is then placed between <em> and </em> tags only.

Other concatenated variables ($link, $session_img, $invisibility_span_open/close, $user_link) are not shown; if any carries unsanitized attacker-controlled data, that would be a separate concern outside this snippet. Also assumes $charset matches page encoding (typically UTF-8 in Chamilo).

document.php → DocumentManager::get_document_data_by_id → document_listing_display (comment rendering branch)


Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6
Language: C
File: net/ceph/auth_x.c
Function: ceph_x_decrypt

Call path: ceph_x_handle_reply (net/ceph/auth_x.c) → ceph_x_proc_ticket_reply (net/ceph/auth_x.c) → process_one_ticket (net/ceph/auth_x.c) → ceph_x_decrypt (net/ceph/auth_x.c) → ceph_decode_copy (include/linux/ceph/decode.h)

### Primary Function

```c
static int ceph_x_decrypt(struct ceph_crypto_key *secret,
			  void **p, void *end, void *obuf, size_t olen)
{
	struct ceph_x_encrypt_header head;
	size_t head_len = sizeof(head);
	int len, ret;

	len = ceph_decode_32(p);
	if (*p + len > end)
		return -EINVAL;

	dout("ceph_x_decrypt len %d\n", len);
	ret = ceph_decrypt2(secret, &head, &head_len, obuf, &olen,
			    *p, len);
	if (ret)
		return ret;
	if (head.struct_v != 1 || le64_to_cpu(head.magic) != CEPHX_ENC_MAGIC)
		return -EPERM;
	*p += len;
	return olen;
}
```

### Cross-File Context

[TEMP_TICKET_BUF_LEN — macro — net/ceph/auth_x.c:16]
TEMP_TICKET_BUF_LEN → 256  (net/ceph/auth_x.c:16)

[CEPHX_ENC_MAGIC — constant — net/ceph/auth_x_protocol.h:83]
CEPHX_ENC_MAGIC → 0xff009cad8826aa55ull  (net/ceph/auth_x_protocol.h:83)

[ceph_x_encrypt_header — struct — net/ceph/auth_x_protocol.h:85-88]
```c
struct ceph_x_encrypt_header {\n\t__u8 struct_v;\n\t__le64 magic;\n} __attribute__ ((packed));
```

[ceph_decode_copy — sink — include/linux/ceph/decode.h:41-45]
ceph_decode_copy → static inline void ceph_decode_copy(void **p, void *pv, size_t n)\n{\n\tmemcpy(pv, *p, n);\n\t*p += n;\n}  (include/linux/ceph/decode.h:41-45)

[ceph_decode_32 — function — include/linux/ceph/decode.h:23-28]
```c
static inline u32 ceph_decode_32(void **p)\n{\n\tu32 v = get_unaligned_le32(*p);\n\t*p += sizeof(u32);\n\treturn v;\n}
```

[ceph_decode_8_safe — macro — include/linux/ceph/decode.h:76-80]
ceph_decode_8_safe → #define ceph_decode_8_safe(p, end, v, bad)\t \t\n\tdo {\t\t\t\t\t\t\t\t\t\n\t\tceph_decode_need(p, end, sizeof(u8), bad);\t\n\t\tv = ceph_decode_8(p);\t\t\t\t\n\t} while (0)  (include/linux/ceph/decode.h:76-80)

[ceph_decode_32_safe — macro — include/linux/ceph/decode.h:66-70]
ceph_decode_32_safe → #define ceph_decode_32_safe(p, end, v, bad)\t \t\n\tdo {\t\t\t\t\t\t\t\t\t\n\t\tceph_decode_need(p, end, sizeof(u32), bad);\t\n\t\tv = ceph_decode_32(p);\t\t\t\t\n\t} while (0)  (include/linux/ceph/decode.h:66-70)

[ceph_decode_need — macro — include/linux/ceph/decode.h:55-59]
ceph_decode_need → #define ceph_decode_need(p, end, n, bad)\t\t\t\n\tdo {\t\t\t\t\t\t\t\t\n\t\tif (!likely(ceph_has_room(p, end, n)))\t\t\n\t\t\tgoto bad;\t\t\t\t\t\n\t} while (0)  (include/linux/ceph/decode.h:55-59)

Control flow: ceph_x_handle_reply dispatches to ceph_x_proc_ticket_reply which iterates over ticket replies calling process_one_ticket for each. process_one_ticket calls ceph_x_decrypt to decrypt ticket data. The pre-fix code passes a fixed 256-byte buffer to ceph_x_decrypt which is insufficient for large tickets. ceph_decrypt2 internally calls ceph_decode_copy which performs the unbounded memcpy causing overflow.

Data flow: The ticket data originates from the Ceph Monitor in the network protocol. It is decoded from wire format via ceph_decode_32() to determine length 'dlen'. In the pre-fix code, this dlen value controls a memcpy into a 256-byte fixed buffer without validation against TEMP_TICKET_BUF_LEN. The attacker controls the ticket length in the protocol, enabling arbitrary overflow up to available memory.

Verdict: Vulnerable
Vulnerable region:
[net/ceph/auth_x.c]
```c
static int ceph_x_decrypt(struct ceph_crypto_key *secret,
			  void **p, void *end, void *obuf, size_t olen)
{
	struct ceph_x_encrypt_header head;
	size_t head_len = sizeof(head);
	int len, ret;

	len = ceph_decode_32(p);
	if (*p + len > end)
		return -EINVAL;

	dout("ceph_x_decrypt len %d\n", len);
	ret = ceph_decrypt2(secret, &head, &head_len, obuf, &olen,
			    *p, len);
	if (ret)
		return ret;
	if (head.struct_v != 1 || le64_to_cpu(head.magic) != CEPHX_ENC_MAGIC)
		return -EPERM;
	*p += len;
	return olen;
}
```
CWE: CWE-399
Severity: HIGH

Root cause: The TEMP_TICKET_BUF_LEN macro hardcodes a 256-byte buffer for cephx auth ticket decryption. The function ceph_x_decrypt receives obuf as a caller-allocated fixed-size buffer. When the actual encrypted/decrypted data length exceeds 256 bytes, ceph_decode_copy() in ceph_decrypt2 writes beyond the buffer boundary. This occurs in process_one_ticket when handling unencrypted tickets (is_enc=0) at line 213: ceph_decode_copy(p, ticket_buf, dlen) copies dlen bytes into ticket_buf which is allocated as TEMP_TICKET_BUF_LEN=256.

Exploitability: An attacker controlling a Ceph Monitor sends a crafted CEPHX auth ticket reply containing an unencrypted ticket blob with size > 256 bytes. The ceph_x_decrypt function uses a fixed 256-byte stack buffer, and the underlying ceph_decode_copy() performs an unbounded memcpy into this buffer, resulting in buffer overflow.

Attack preconditions:
- Attacker must control or compromise a Ceph Monitor to send crafted auth replies
- Target must be a Linux kernel Ceph client attempting authentication with the attacker-controlled monitor
- The crafted ticket must have is_enc=0 (unencrypted) with dlen > TEMP_TICKET_BUF_LEN (256 bytes)

Likely impact: An attacker controlling a Ceph Monitor sends a crafted CEPHX auth ticket reply containing an unencrypted ticket blob with size > 256 bytes. The ceph_x_decrypt function uses a fixed 256-byte stack buffer, and the underlying ceph_decode_copy() performs an unbounded memcpy into this buffer, resulting in buffer overflow.

## Analysis

### Code Flow

`ceph_x_decrypt` reads a 32-bit length from the input buffer:

```c
len = ceph_decode_32(p);
if (*p + len > end)
    return -EINVAL;
```

Then it calls `ceph_decrypt2` with `*p` and `len`.

### Issue: Missing length check on `ceph_decode_32`

Before calling `ceph_decode_32(p)`, there is no verification that `*p + sizeof(u32) <= end`. `ceph_decode_32` unconditionally advances `*p` by 4 bytes and reads via `get_unaligned_le32`. If `*p` is at or near `end`, this could read past the end of the buffer.

The safe pattern (`ceph_decode_32_safe`) checks `ceph_has_room(p, end, sizeof(u32))` first. This function does not.

### Issue: Integer overflow in bounds check

The check:

```c
if (*p + len > end)
    return -EINVAL;
```

`len` is a `u32` controlled by the attacker (it comes from the encrypted-message input). On 32-bit systems, `*p + len` can wrap around. For example, if `*p` is near the high end of the address space and `len` is large (e.g., 0xFFFFFFFF), `*p + len` wraps to a small address that is less than `end`, bypassing the check. Then `ceph_decrypt2` is called with a `len` that exceeds the actual buffer size.

The safe idiom is `len > end - *p`, used elsewhere in the kernel for exactly this reason.

### Attacker influence

`ceph_x_decrypt` processes data received from the Ceph monitor / authentication server during `ceph_x_handle_reply`. A malicious or compromised Ceph mon/auth server (or a network attacker that can inject auth replies) can supply arbitrary `len` values.

### Impact

- The integer overflow on 32-bit systems lets the attacker pass a `len` larger than the remaining buffer to `ceph_decrypt2`, leading to out-of-bounds read of kernel memory (and possible decryption of OOB data, or crash).
- The missing 4-byte length-prefix bounds check allows a small (up to 4 byte) OOB read of the length field, less impactful but still a bug.

### Verdict

This is a real vulnerability. The primary issue is the unsafe pointer-arithmetic bounds check `*p + len > end`, which is subject to integer overflow with attacker-controlled `len`, allowing the subsequent `ceph_decrypt2` call to read out-of-bounds memory. A secondary issue is that `ceph_decode_32` is used without a `_safe` variant.

**Classification:** CWE-190 (Integer Overflow) leading to CWE-125 (Out-of-bounds Read) / CWE-119 (Improper buffer access). This is the well-known class of "pointer + attacker-controlled length > end" overflow bugs.

**Fix sketch:** Replace the manual decode + check with `ceph_decode_32_safe(p, end, len, e_inval); if (len > end - *p) return -EINVAL;`.

**Vulnerable: yes.**

The bounds check '*p + len > end' performs unchecked pointer arithmetic with an attacker-controlled 32-bit length. On 32-bit kernels (and at high virtual addresses), '*p + len' can wrap around to a value less than 'end', bypassing the bounds check. Additionally, ceph_decode_32() is invoked without first verifying 4 bytes of room, allowing a small OOB read of the length prefix.

An attacker who can supply or tamper with Ceph authentication replies (a malicious/compromised Ceph mon or auth server, or an on-path attacker injecting auth traffic) can craft a reply whose embedded 32-bit length triggers the overflow. On 32-bit systems, choosing len near 0xFFFFFFFF makes (*p + len) wrap below 'end', causing the subsequent ceph_decrypt2() to read past the input buffer. The safe idiom 'len > end - *p' is not used.

Kernel built with CONFIG_CEPH_LIB and a Ceph client connected to an attacker-influenced mon/auth server (or attacker able to inject/forge auth reply traffic). Practical exploitability of integer wrap requires a 32-bit kernel; the missing 4-byte length prefix check applies broadly.

Out-of-bounds read of kernel memory passed to the crypto routine, possible information disclosure of decrypted OOB bytes through subsequent ticket parsing, and potential kernel crash (DoS) if the OOB read crosses an unmapped page.

All bounds checks must be expressed without pointer overflow (e.g., 'len > end - *p') and any decode of N bytes from the input buffer must first verify N bytes of room (ceph_decode_*_safe / ceph_decode_need).

Both invariants are violated: ceph_decode_32() is called without ceph_decode_need/ceph_decode_32_safe, and the post-decode bounds check uses '*p + len > end' which is subject to integer/pointer overflow when len is attacker controlled.

Reachability depends on whether the function is called before or after server authentication of the message; the supplied call path indicates it processes ticket replies during auth handshake, prior to establishing trust. 64-bit kernels make the wrap less practical, but the missing 4-byte ceph_decode_32 length check still permits small OOB reads.

ceph_x_handle_reply -> ceph_x_proc_ticket_reply -> process_one_ticket -> ceph_x_decrypt -> ceph_decode_32 / ceph_decrypt2


Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux
Language: C
File: drivers/scsi/scsi_transport_iscsi.c
Function: iscsi_set_param

Call path: iscsi_if_rx (drivers/scsi/scsi_transport_iscsi.c) → iscsi_if_recv_msg (drivers/scsi/scsi_transport_iscsi.c) → iscsi_set_param (drivers/scsi/scsi_transport_iscsi.c) → transport->set_param (drivers/scsi/libiscsi.c) → iscsi_session_get_param (drivers/scsi/libiscsi.c) → iscsi_conn_get_param (drivers/scsi/libiscsi.c) → iscsi_host_get_param (drivers/scsi/libiscsi.c)

### Primary Function

```c
static int
iscsi_set_param(struct iscsi_transport *transport, struct iscsi_uevent *ev)
{
	char *data = (char*)ev + sizeof(*ev);
	struct iscsi_cls_conn *conn;
	struct iscsi_cls_session *session;
	int err = 0, value = 0;

	if (ev->u.set_param.len > PAGE_SIZE)
		return -EINVAL;

	session = iscsi_session_lookup(ev->u.set_param.sid);
	conn = iscsi_conn_lookup(ev->u.set_param.sid, ev->u.set_param.cid);
	if (!conn || !session)
		return -EINVAL;

	switch (ev->u.set_param.param) {
	case ISCSI_PARAM_SESS_RECOVERY_TMO:
		sscanf(data, "%d", &value);
		if (!session->recovery_tmo_sysfs_override)
			session->recovery_tmo = value;
		break;
	default:
		err = transport->set_param(conn, ev->u.set_param.param,
					   data, ev->u.set_param.len);
	}

	return err;
}
```

### Cross-File Context

[iscsi_set_host_param — function — drivers/scsi/scsi_transport_iscsi.c:3026]
```c
static int
iscsi_set_host_param(struct iscsi_transport *transport,
		     struct iscsi_uevent *ev)
{
	char *data = (char*)ev + sizeof(*ev);
	struct Scsi_Host *shost;
	int err;

	if (!transport->set_host_param)
		return -ENOSYS;

	if (ev->u.set_host_param.len > PAGE_SIZE)
		return -EINVAL;

	shost = scsi_host_lookup(ev->u.set_host_param.host_no);
	if (!shost) {
		printk(KERN_ERR "set_host_param could not find host no %u\n",
		       ev->u.set_host_param.host_no);
		return -ENODEV;
	}

	err = transport->set_host_param(shost, ev->u.set_host_param.param,
					data, ev->u.set_host_param.len);
	scsi_host_put(shost);
	return err;
}
```

[sysfs_emit — function — fs/sysfs/file.c:732]
```c
int sysfs_emit(char *buf, const char *fmt, ...)
{
	va_list args;
	int len;

	if (WARN(!buf || offset_in_page(buf),
		 "invalid sysfs_emit: buf:%p\n", buf))
		return 0;

	va_start(args, fmt);
	len = vscnprintf(buf, PAGE_SIZE, fmt, args);
	va_end(args);

	return len;
}
```

[PAGE_SIZE — constant — arch/x86/include/asm/page_size.h:4]
PAGE_SIZE → 4096  (arch/x86/include/asm/page_size.h:4)

[struct iscsi_uevent — struct — include/scsi/iscsi_if.h:93]
```c
struct iscsi_uevent {
	uint32_t type;
	uint32_t iferror;
	uint64_t transport_handle;

	union {
		struct msg_set_param {
			uint32_t sid;
			uint32_t cid;
			uint32_t param;
			uint32_t len;
		} set_param;
		/* ... other fields ... */
	} u;
}
```

[enum iscsi_param — enum — include/scsi/iscsi_if.h:526]
enum iscsi_param { ISCSI_PARAM_MAX_RECV_DLENGTH, ISCSI_PARAM_MAX_XMIT_DLENGTH, /* ... */ ISCSI_PARAM_USERNAME, ISCSI_PARAM_PASSWORD, ISCSI_PARAM_TARGET_NAME, /* ... */ ISCSI_PARAM_MAX, };

Control flow: The function extracts a data pointer from the uevent structure and immediately validates the user-controlled length field `ev->u.set_param.len` against `PAGE_SIZE`, returning `-EINVAL` if the limit is exceeded. It then performs kernel object lookups for the session and connection, returning `-EINVAL` if either lookup fails. Execution branches based on `ev->u.set_param.param`: for `ISCSI_PARAM_SESS_RECOVERY_TMO`, it parses an integer via `sscanf` and conditionally updates a session timeout field; for all other parameters, it delegates to `transport->set_param`. The function concludes by returning the accumulated error code.

Data flow: The input `ev` structure provides the payload length (`ev->u.set_param.len`), parameter identifier, and session/connection IDs. `data` is derived as a pointer to the memory region immediately following the `ev` header. The length field is validated early. In the recovery timeout case, `data` is parsed into a local integer `value`. In the default case, `data` and the validated `len` are forwarded to the transport callback. The only state modifications occur on the `session` struct or are passed through to the callee, with all external inputs bounded before consumption.

Verdict: Not Vulnerable

Reviewed region:
[drivers/scsi/scsi_transport_iscsi.c]
```c
static int
iscsi_set_param(struct iscsi_transport *transport, struct iscsi_uevent *ev)
{
	char *data = (char*)ev + sizeof(*ev);
	struct iscsi_cls_conn *conn;
	struct iscsi_cls_session *session;
	int err = 0, value = 0;

	if (ev->u.set_param.len > PAGE_SIZE)
		return -EINVAL;

	session = iscsi_session_lookup(ev->u.set_param.sid);
	conn = iscsi_conn_lookup(ev->u.set_param.sid, ev->u.set_param.cid);
	if (!conn || !session)
		return -EINVAL;

	switch (ev->u.set_param.param) {
	case ISCSI_PARAM_SESS_RECOVERY_TMO:
		sscanf(data, "%d", &value);
		if (!session->recovery_tmo_sysfs_override)
			session->recovery_tmo = value;
		break;
	default:
		err = transport->set_param(conn, ev->u.set_param.param,
					   data, ev->u.set_param.len);
	}

	return err;
}
```

Why it is not vulnerable: The code explicitly validates the user-controlled `ev->u.set_param.len` against `PAGE_SIZE` before any parsing, state modification, or callback delegation occurs. This early bounds check prevents oversized payloads from triggering buffer overflows or memory corruption. Additionally, mandatory null checks on `session` and `conn` prevent invalid pointer dereferences, and the format string used for integer parsing is strict and safe.

Security invariants:
- User-controlled payload length must not exceed `PAGE_SIZE` before any processing. Enforced by `if (ev->u.set_param.len > PAGE_SIZE) return -EINVAL;`.
- Kernel object pointers must be valid before access or delegation. Enforced by `if (!conn || !session) return -EINVAL;`.
- Downstream callbacks must receive a validated length to prevent unbounded reads/writes. Enforced by passing `ev->u.set_param.len` to `transport->set_param` only after the `PAGE_SIZE` gate.
- String-to-integer parsing must be bounded and use safe formats. Enforced by the `PAGE_SIZE` upper limit on `data` and the strict `"%d"` format specifier in `sscanf`.

Invariant verification:
- Length bounds validation: holds=true. Evidence: The condition `if (ev->u.set_param.len > PAGE_SIZE) return -EINVAL;` executes immediately after data extraction, rejecting oversized inputs before any usage.
- Null pointer safety: holds=true. Evidence: The lookup results are checked via `if (!conn || !session) return -EINVAL;` prior to dereferencing `session->recovery_tmo_sysfs_override` or passing `conn` to the callback.
- Safe payload delegation: holds=true. Evidence: The validated `len` is explicitly passed to `transport->set_param(conn, ev->u.set_param.param, data, ev->u.set_param.len)`, ensuring downstream handlers operate on a strictly bounded size.

Residual risk notes: The `sscanf` call for `ISCSI_PARAM_SESS_RECOVERY_TMO` does not explicitly use the `len` field to limit parsing, relying instead on the payload's implicit null-termination and the `PAGE_SIZE` upper bound. While acceptable given the strict length gate, using size-aware parsing would be marginally more robust. Additionally, the safety of the `default` branch inherently relies on `transport->set_param` correctly respecting the `len` parameter it receives.

No concrete vulnerability impact is demonstrated in the supplied code and context.

## Analysis

### Code Behavior

`iscsi_set_param` receives a netlink message via `iscsi_if_rx` → `iscsi_if_recv_msg`. It treats the bytes after the `iscsi_uevent` header as `data` and forwards `(data, ev->u.set_param.len)` to the transport-specific `set_param` callback.

Key checks:
1. `ev->u.set_param.len > PAGE_SIZE` → returns `-EINVAL`. So `len` is bounded to ≤ 4096.
2. session/conn lookup must succeed.
3. For `ISCSI_PARAM_SESS_RECOVERY_TMO`, it does `sscanf(data, "%d", &value)`.

### Concerns to Examine

**1. `sscanf(data, "%d", &value)` without explicit length:**
`sscanf` reads from `data` looking for digits. The data buffer is the netlink skb payload after the uevent header. There is no explicit guarantee `data` is NUL-terminated. However:
- `%d` parses a number and stops at any non-digit character. It does not depend on NUL termination per se, but it will read until a non-digit byte is found. If the netlink payload has no non-digit byte before the buffer ends, it could read OOB.
- In `iscsi_if_recv_msg`, the caller validates `nlh->nlmsg_len` against the skb. The `data` pointer points into the skb; reads beyond the allocated skb data are theoretically possible, but typically the skb is page-aligned/sized and `len` ≤ PAGE_SIZE means `data + len` is within bounds.

This is suspicious but the actual exploit window depends on the caller's validation in `iscsi_if_recv_msg`, which is not shown. Historically this is a known mild concern but not generally CVE-rated.

**2. Length parameter passed to `transport->set_param`:**
`len` is bounded by PAGE_SIZE and the data size in the skb is verified by the netlink layer in `iscsi_if_recv_msg`. The transport handler decides how to use the data.

**3. Missing validation of `ev->u.set_param.param`:**
The `param` value is forwarded to the transport without bounds-checking against `ISCSI_PARAM_MAX`. The transport's `set_param` is expected to handle this. Not directly a vulnerability in this function.

**4. Authorization / capability checks:**
The function does no capability check itself. However, `iscsi_if_rx` typically validates `CAP_SYS_ADMIN` via `netlink_capable` upstream — this is the kernel iSCSI netlink contract. Not visible in this snippet.

### Evidence vs. Conclusion

**Observed:**
- `len` is checked against `PAGE_SIZE`.
- `data` pointer arithmetic `(char*)ev + sizeof(*ev)` is straightforward.
- `sscanf("%d")` operates on possibly-non-NUL-terminated input but bounded by typical skb allocation.
- Conn/session lookups are performed but no reference counting is shown to be incremented/decremented in this snippet — note `iscsi_set_host_param` does `scsi_host_lookup`/`scsi_host_put`, but `iscsi_session_lookup`/`iscsi_conn_lookup` here do not show matching puts. This could be a use-after-free / refcount issue if these lookups acquire references, but in iscsi_transport these specific lookups historically don't take refs (they're used under a lock asserted by the caller).

**No clear vulnerability evidence:**
- The size check prevents oversized copies.
- No buffer overflow in this function's own logic.
- The `sscanf` concern is minor and not clearly exploitable from the snippet — it depends on skb bounds in the caller.
- No NULL dereference (both pointers checked).

The code is consistent with the established netlink iSCSI handler pattern, with the standard guards (length cap, lookup checks). Anything beyond that depends on `iscsi_if_recv_msg` (caller) and `transport->set_param` (callee), which are out of scope.

### Verdict

**Not vulnerable** based on the supplied code.

The visible invariants — `len ≤ PAGE_SIZE`, `conn` and `session` non-NULL checks, and forwarding to a transport-specific handler — are sufficient for this function's responsibility. The `sscanf("%d")` on non-NUL-terminated data is a minor robustness concern but not a clearly exploitable vulnerability given that the netlink payload length is validated upstream and `data` lies inside an skb buffer of at least `len` bytes.

**Residual uncertainty:**
- Caller `iscsi_if_recv_msg` is not shown; if it fails to ensure `data + len` lies within the skb, the `sscanf` could read slightly OOB (low impact, kernel info-leak at best).
- Whether `iscsi_session_lookup`/`iscsi_conn_lookup` take references that are leaked here. The mirror function `iscsi_set_host_param` shows `scsi_host_put` is required after `scsi_host_lookup`; the absence of analogous puts here would only be a bug if those lookups bump refcounts, which the supplied context does not confirm.
- Capability checks for the netlink socket are assumed to live in `iscsi_if_rx`.

No concrete vulnerability is demonstrable from the supplied code.

drivers/scsi/scsi_transport_iscsi.c:iscsi_set_param (full body shown)

No exploitable path is demonstrable from the supplied code. The function bounds the user-supplied length (len <= PAGE_SIZE), null-checks both session and conn after lookup, and delegates to the transport-specific handler. The sscanf("%d") on a possibly non-NUL-terminated buffer is constrained by the upstream netlink skb length validation expected in iscsi_if_recv_msg.

Would require a netlink client with CAP_SYS_ADMIN (enforced upstream in iscsi_if_rx per the iSCSI netlink contract) plus an additional defect in either the caller (skb bounds) or the transport->set_param callback. Neither is visible here.

The function performs the required guards within its own responsibility: (1) ev->u.set_param.len is rejected if it exceeds PAGE_SIZE, preventing oversized copies downstream; (2) both iscsi_session_lookup and iscsi_conn_lookup results are checked for NULL before use; (3) the only direct parsing it performs is sscanf("%d") which writes to a local int and has no buffer write side effect. Any further validation of param semantics or data contents is the responsibility of the transport callback. The mirror routine iscsi_set_host_param shows scsi_host_put because scsi_host_lookup takes a reference; iscsi_session_lookup/iscsi_conn_lookup in this subsystem do not take references (they walk lists under the iscsi transport lock held by the caller), so the absence of a put here is consistent and not a leak/UAF.

User-controlled length is capped at PAGE_SIZE before any downstream use.

session and conn pointers are dereferenced only after non-NULL check.

Only a stack int is written by sscanf; data buffer is read-only here.

Capability/permission enforcement for the netlink socket is performed by the caller (iscsi_if_rx).

Verified by direct inspection: the `if (ev->u.set_param.len > PAGE_SIZE) return -EINVAL;` check and the `if (!conn || !session) return -EINVAL;` check both appear before any use. sscanf writes to local `value` only. Length is forwarded unchanged to transport->set_param, which is responsible for its own bounds.

Not visible in the snippet: (a) whether iscsi_if_recv_msg ensures data+len lies within the skb (a defect there could allow a small OOB read by sscanf scanning past the payload, low-impact info disclosure at worst); (b) correctness of each transport->set_param implementation when handed up to PAGE_SIZE bytes; (c) capability gating in iscsi_if_rx. None of these are defects in iscsi_set_param itself.

iscsi_if_rx -> iscsi_if_recv_msg -> iscsi_set_param -> transport->set_param


Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: github.com/privacyidea/privacyidea
Language: Python
File: privacyidea/lib/decorators.py
Function: check_user_or_serial_in_request_wrapper

Call path: offlinerefill (privacyidea/api/validate.py) → check_user_or_serial_in_request_wrapper (privacyidea/lib/decorators.py) → get_tokens (privacyidea/lib/token.py)

### Primary Function

```python
def check_user_or_serial_in_request_wrapper(*args, **kwds):
    user = self.request.all_data.get("user")
    serial = self.request.all_data.get("serial")
    if not serial and not user:
        raise ParameterError(_("You need to specify a serial or a user."))
    f_result = func(*args, **kwds)
    return f_result
```

### Cross-File Context

[check_user_or_serial_in_request — class — privacyidea/lib/decorators.py:75-94]
class check_user_or_serial_in_request(object): """ Decorator to check user and serial in a request. If the request does not contain a serial number (serial) or a user (user) it will throw a ParameterError. """ def __init__(self, request): self.request = request def __call__(self, func): @functools.wraps(func) def check_user_or_serial_in_request_wrapper(*args, **kwds): user = self.request.all_data.get("user") serial = self.request.all_data.get("serial") if not serial and not user: raise ParameterError(_("You need to specify a serial or a user.")) f_result = func(*args, **kwds) return f_result return check_user_or_serial_in_request_wrapper

[ParameterError — exception — privacyidea/lib/error.py]
class ParameterError(Exception)

Control flow: HTTP request enters at offlinerefill endpoint, passes through @check_user_or_serial_in_request decorator which validates user/serial parameters, then calls get_tokens(serial=serial) which performs LDAP query using unsanitized serial parameter in pre-fix version.

Data flow: Attacker-controlled user/serial HTTP parameters flow through request.all_data.get() to get_tokens() LDAP query without validation or sanitization for LDAP wildcard characters in pre-fix version.

Verdict: Vulnerable
Vulnerable region:
[privacyidea/lib/decorators.py]
```python
def check_user_or_serial_in_request_wrapper(*args, **kwds):
            user = self.request.all_data.get("user")
            serial = self.request.all_data.get("serial")
            if not serial and not user:
                raise ParameterError(_("You need to specify a serial or a user."))
            f_result = func(*args, **kwds)
            return f_result
```
CWE: CWE-20
Severity: HIGH

Root cause: The check_user_or_serial_in_request decorator's __call__ method retrieves user and serial from request.all_data using .get() without a default value and without stripping whitespace or validating for LDAP wildcard characters. When these unsanitized values are passed to downstream LDAP query functions, an attacker can inject LDAP wildcards (% for any character sequence, * for any single character) to manipulate query results or bypass authentication checks.

Exploitability: The vulnerability is exploitable via HTTP POST requests to /validate/offlinerefill (missing decorator) or via the existing /validate/check endpoint (decorator present but insufficient validation). An attacker can inject LDAP wildcards (% or *) in the user or serial parameters to manipulate LDAP queries.

Attack preconditions:
- Attacker can send HTTP requests to the privacyIDEA /validate/offlinerefill or /validate/check endpoints
- Attacker provides user parameter containing '%' character or serial parameter containing '*' character
- The vulnerable endpoint processes these parameters in LDAP queries without sanitization

Likely impact: The vulnerability is exploitable via HTTP POST requests to /validate/offlinerefill (missing decorator) or via the existing /validate/check endpoint (decorator present but insufficient validation). An attacker can inject LDAP wildcards (% or *) in the user or serial parameters to manipulate LDAP queries.

## Analysis

### Code Behavior

The decorator `check_user_or_serial_in_request` wraps a function so that, before invocation, it checks whether `request.all_data` contains either a `serial` or a `user` parameter. If neither is supplied, it raises `ParameterError`. Otherwise, the wrapped function executes.

### Call Path Context

The call path is:
`offlinerefill` → `check_user_or_serial_in_request_wrapper` → `get_tokens`

This means a request to `offlinerefill` is gated by the requirement that the caller has provided at least one of `user` or `serial`, before tokens are looked up.

### Security-Relevant Observations

1. **Input validation**: The decorator only enforces presence of one of the two parameters. It does not authenticate the caller, validate authorization, or check the relationship between the user and the serial. That is by design — it's a parameter-presence check, nothing more.

2. **Logical correctness of the guard**: `not serial and not user` correctly raises only when *both* are missing. Truthiness check rejects empty strings (`""`), `None`, and missing keys. There is no off-by-one or inversion bug.

3. **No injection / no taint propagation**: The values are not concatenated into queries, file paths, shell commands, or formatting in this function. They are simply read and tested for truthiness. The downstream function (`get_tokens`) is responsible for its own handling of these parameters.

4. **No authorization bypass surface here**: The decorator does not claim to be an authz check; it's a pre-condition validator. Any actual authorization is presumably done elsewhere (other decorators on `offlinerefill`, or inside `get_tokens` / token policies).

5. **No DoS/resource issue**: Trivial dict lookups; raises an exception cheaply when input is malformed.

6. **Error handling**: Raising `ParameterError` is consistent with the project pattern. The error message is internationalized and reveals nothing sensitive.

### Possible Concerns Considered and Dismissed

- *Type confusion*: If `request.all_data["user"]` were a non-string (e.g., a list), `not user` still evaluates truthiness sanely. No crash here.
- *Empty-but-present values*: `user=""` is treated as missing — that is the desired behavior for a presence check.
- *Bypass via providing both*: Providing both is allowed; whatever logic resolves precedence lives in `get_tokens` or `offlinerefill`. No vulnerability in *this* function flows from that.
- *Race conditions*: None; purely synchronous read of a request-scoped dict.

### Conclusion

This is a small, defensive parameter-presence decorator. The supplied code does not perform any operation (string interpolation, authorization decision, deserialization, file/network/DB access) that could host a vulnerability of its own. Any actual security property of `offlinerefill` (e.g., that the caller is allowed to refill the specified serial's offline OTPs) must be enforced by other decorators or inside `get_tokens`/the offline-refill logic — and the absence of such checks would be a finding against *those* sites, not this one.

**Verdict: Not vulnerable.** The function correctly performs its narrow contract (require `serial` or `user`) with no exploitable behavior in the code shown.

**Residual uncertainty**: The supplied context does not include `offlinerefill` itself or the full decorator stack on it. If `offlinerefill` relies *solely* on this decorator for security and lacks any authentication/authorization decorator, that would be a vulnerability in the route definition — not in this wrapper. From the code given, no such conclusion can be drawn.

privacyidea/lib/decorators.py:75-94 (check_user_or_serial_in_request decorator class and its wrapper)

No exploitable behavior in the supplied code. The wrapper performs only dict lookups and a truthiness check; it does not interpolate, deserialize, authorize, or otherwise process the values in a way that could be abused.

The decorator's sole job is to ensure that at least one of `user` or `serial` is present in `request.all_data` and to raise `ParameterError` otherwise. The boolean expression `not serial and not user` correctly raises only when both are missing/empty/None. Values are not used in any sink (no SQL, shell, path, format string, eval, or auth decision) inside this function. Downstream handling is the responsibility of `get_tokens` and other decorators on `offlinerefill`.

At least one of `user` or `serial` must be truthy in `request.all_data` before the wrapped function is invoked.

Empty string, None, and missing key are uniformly treated as 'not provided'.

The wrapper does not transform or propagate the values to any sensitive sink within its own body.

The guard `if not serial and not user: raise ParameterError(...)` enforces the presence invariant. The lookups use `.get()` so missing keys yield `None`, which is falsy. No code path bypasses the check before `func(*args, **kwds)` is called.

Supplied context does not include `offlinerefill` itself or its full decorator stack. If `offlinerefill` relies solely on this presence check without separate authentication/authorization (e.g., ensuring the caller owns or is permitted to refill the given serial), that would be a finding against the route definition or `get_tokens`/policy layer, not this decorator. Cannot be concluded from the code shown.

offlinerefill (privacyidea/api/validate.py) -> check_user_or_serial_in_request_wrapper (privacyidea/lib/decorators.py) -> get_tokens (privacyidea/lib/token.py)


Audit the following candidate code to determine whether it presents a real vulnerability.
This sample may come from a noisy retrieval or transformation stage, so reason from the supplied code and context rather than assuming the label.

Repository: git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux
Language: C
File: net/ipv4/tcp_input.c
Function: tcp_cwnd_reduction

Call path: tcp_enter_cwr (net/ipv4/tcp_input.c) → tcp_init_cwnd_reduction (net/ipv4/tcp_input.c) → tcp_cwnd_reduction (net/ipv4/tcp_input.c) → tcp_try_keep_open (net/ipv4/tcp_input.c) → tcp_process_loss (net/ipv4/tcp_input.c) → tcp_fastretrans_alert (net/ipv4/tcp_input.c)

### Primary Function

```c
static void tcp_cwnd_reduction(struct sock *sk, const int prior_unsacked,
				       int fast_rexmit, int flag)
{
	struct tcp_sock *tp = tcp_sk(sk);
	int sndcnt = 0;
	int delta = tp->snd_ssthresh - tcp_packets_in_flight(tp);
	int newly_acked_sacked = prior_unsacked -
				 (tp->packets_out - tp->sacked_out);

	if (newly_acked_sacked <= 0 || WARN_ON_ONCE(!tp->prior_cwnd))
		return;

	tp->prr_delivered += newly_acked_sacked;
	if (delta < 0) {
		u64 dividend = (u64)tp->snd_ssthresh * tp->prr_delivered +
			       tp->prior_cwnd - 1;
		sndcnt = div_u64(dividend, tp->prior_cwnd) - tp->prr_out;
	} else if ((flag & FLAG_RETRANS_DATA_ACKED) &&
		   !(flag & FLAG_LOST_RETRANS)) {
		sndcnt = min_t(int, delta,
			       max_t(int, tp->prr_delivered - tp->prr_out,
				     newly_acked_sacked) + 1);
	} else {
		sndcnt = min(delta, newly_acked_sacked);
	}
	sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0));
	tp->snd_cwnd = tcp_packets_in_flight(tp) + sndcnt;
}
```

### Cross-File Context

[tcp_init_cwnd_reduction — helper — net/ipv4/tcp_input.c:2458-2470]
```c
static void tcp_init_cwnd_reduction(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);

	tp->high_seq = tp->snd_nxt;
	tp->tlp_high_seq = 0;
	tp->snd_cwnd_cnt = 0;
	tp->prior_cwnd = tp->snd_cwnd;
	tp->prr_delivered = 0;
	tp->prr_out = 0;
	tp->snd_ssthresh = inet_csk(sk)->icsk_ca_ops->ssthresh(sk);
	tcp_ecn_queue_cwr(tp);
}
```

[tcp_packets_in_flight — function — include/net/tcp.h:997-1001]
```c
static inline unsigned int tcp_packets_in_flight(const struct tcp_sock *tp)
{
	return tp->packets_out - tcp_left_out(tp) + tp->retrans_out;
}
```

[div_u64 — function — include/linux/math64.h:96-100]
```c
static inline u64 div_u64(u64 dividend, u32 divisor)
{
	u32 remainder;
	return div_u64_rem(dividend, divisor, &remainder);
}
```

[WARN_ON_ONCE — macro — include/asm-generic/bug.h:109-118]
WARN_ON_ONCE → #define WARN_ON_ONCE(condition) ({ \ static bool __section(.data.unlikely) __warned; \ int __ret_warn_once = !!(condition); \ \ if (unlikely(__ret_warn_once)) \ if (WARN_ON(!__warned)) \ __warned = true; \ unlikely(__ret_warn_once); \ })  (include/asm-generic/bug.h:109-118)

[struct tcp_sock — struct — include/linux/tcp.h:132-350]
```c
struct tcp_sock {
	/* inet_connection_sock has to be the first member of tcp_sock */
	struct inet_connection_sock	inet_conn;
	u16	tcp_header_len;
	u16	gso_segs;
	__be32	pred_flags;
	u64	bytes_received;
	u32	segs_in;
	u32	rcv_nxt;
	u32	copied_seq;
	u32	rcv_wup;
	u32	snd_nxt;
	u32	segs_out;
	u64	bytes_acked;
	struct u64_stats_sync syncp;
	u32	snd_una;
	u32	snd_sml;
	u32	rcv_tstamp;
	u32	lsndtime;
	u32	last_oow_ack_time;
	u32	tsoffset;
	struct list_head tsq_node;
	unsigned long	tsq_flags;
	struct {
		struct sk_buff_head	prequeue;
		struct task_struct	*task;
		struct msghdr		*msg;
		int			memory;
		int			len;
	} ucopy;
	u32	snd_wl1;
	u32	snd_wnd;
	u32	max_window;
	u32	mss_cache;
	u32	window_clamp;
	u32	rcv_ssthresh;
	struct tcp_rack rack;
	u16	advmss;
	u8	unused;
	u8	nonagle     : 4;
	u8	thin_lto    : 1;
	u8	thin_dupack : 1;
	u8	repair      : 1;
	u8	frto        : 1;
	u8	repair_queue;
	u8	do_early_retrans:1;
	u8	syn_data:1;
	u8	syn_fastopen:1;
	u8	syn_fastopen_exp:1;
	u8	syn_data_acked:1;
	u8	save_syn:1;
	u8	is_cwnd_limited:1;
	u32	tlp_high_seq;
	u32	srtt_us;
	u32	mdev_us;
	u32	mdev_max_us;
	u32	mdev_max_us;
	u32	rttvar_us;
	u32	rtt_seq;
	struct rtt_meas rtt_min[3];
	u32	packets_out;
	u32	retrans_out;
	u32	max_packets_out;
	u32	max_packets_seq;
	u16	urg_data;
	u8	ec n_flags;
	u8	keepalive_probes;
	u32	reordering;
	u32	snd_up;
	struct tcp_options_received rx_opt;
	u32	snd_ssthresh;
	u32	snd_cwnd;
	u32	snd_cwnd_cnt;
	u32	snd_cwnd_clamp;
	u32	snd_cwnd_used;
	u32	snd_cwnd_stamp;
	u32	prior_cwnd;
	u32	prr_delivered;
	u32	prr_out;
	u32	rcv_wnd;
	u32	write_seq;
	u32	notsent_lowat;
	u32	pushed_seq;
	u32	lost_out;
	u32	sacked_out;
	u32	fackets_out;
	struct sk_buff* lost_skb_hint;
	struct sk_buff *retransmit_skb_hint;
	struct sk_buff_head out_of_order_queue;
	struct tcp_sack_block duplicate_sack[1];
	struct tcp_sack_block selective_acks[4];
	struct tcp_sack_block recv_sack_cache[4];
	struct sk_buff *highest_sack;
	int     lost_cnt_hint;
	u32     retransmit_high;
	u32	prior_ssthresh;
	u32	high_seq;
	u32	retrans_stamp;
	u32	undo_marker;
	int	undo_retrans;
	u32	total_retrans;
	/* ... more fields ... */
}
```

[FLAG_RETRANS_DATA_ACKED — constant — net/ipv4/tcp_input.c:109]
FLAG_RETRANS_DATA_ACKED → 0x08 /* Some of which was retransmitted */  (net/ipv4/tcp_input.c:109)

[FLAG_LOST_RETRANS — constant — net/ipv4/tcp_input.c:114]
FLAG_LOST_RETRANS → 0x80 /* This ACK marks some retransmission lost */  (net/ipv4/tcp_input.c:114)

Control flow: The function tcp_cwnd_reduction begins by computing intermediate values (delta, newly_acked_sacked). An early return guard checks whether newly_acked_sacked is positive AND tp->prior_cwnd is non-zero; if either condition fails, the function returns immediately without executing any subsequent logic. When execution continues past the guard, one of three branches computes sndcnt based on delta's sign and flag values. In the delta < 0 branch, div_u64 is called with tp->prior_cwnd as divisor. All three branches flow to updating tp->snd_cwnd as the final operation.

Data flow: Inputs prior_unsacked, fast_rexmit, and flag are combined with tp fields to compute newly_acked_sacked and delta. tp->prior_cwnd originates from tcp_init_cwnd_reduction which sets it to tp->snd_cwnd. The newly_acked_sacked value is added to tp->prr_delivered. When delta < 0, a dividend is computed using tp->snd_ssthresh, tp->prr_delivered, and tp->prior_cwnd, then divided by tp->prior_cwnd via div_u64. The resulting sndcnt (constrained by fast_rexmit and delta logic) is added to tcp_packets_in_flight(tp) to set tp->snd_cwnd.

Verdict: Not Vulnerable

Reviewed region:
[net/ipv4/tcp_input.c]
```c
static void tcp_cwnd_reduction(struct sock *sk, const int prior_unsacked,
				       int fast_rexmit, int flag)
{
	struct tcp_sock *tp = tcp_sk(sk);
	int sndcnt = 0;
	int delta = tp->snd_ssthresh - tcp_packets_in_flight(tp);
	int newly_acked_sacked = prior_unsacked -
				 (tp->packets_out - tp->sacked_out);

	if (newly_acked_sacked <= 0 || WARN_ON_ONCE(!tp->prior_cwnd))
		return;

	tp->prr_delivered += newly_acked_sacked;
	if (delta < 0) {
		u64 dividend = (u64)tp->snd_ssthresh * tp->prr_delivered +
			       tp->prior_cwnd - 1;
		sndcnt = div_u64(dividend, tp->prior_cwnd) - tp->prr_out;
	} else if ((flag & FLAG_RETRANS_DATA_ACKED) &&
		   !(flag & FLAG_LOST_RETRANS)) {
		sndcnt = min_t(int, delta,
			       max_t(int, tp->prr_delivered - tp->prr_out,
				     newly_acked_sacked) + 1);
	} else {
		sndcnt = min(delta, newly_acked_sacked);
	}
	sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0));
	tp->snd_cwnd = tcp_packets_in_flight(tp) + sndcnt;
}
```

Why it is not vulnerable: The division by tp->prior_cwnd in the delta < 0 branch (div_u64(dividend, tp->prior_cwnd)) is protected by an early return guard at the function entry: 'if (newly_acked_sacked <= 0 || WARN_ON_ONCE(!tp->prior_cwnd)) return;'. The short-circuit evaluation of the || operator ensures that if tp->prior_cwnd is zero, the function returns before reaching the division operation. This guard is evaluated before any code path that uses tp->prior_cwnd as a divisor.

Security invariants:
- tp->prior_cwnd must be non-zero before div_u64(dividend, tp->prior_cwnd) is reached. This is enforced by the guard 'if (newly_acked_sacked <= 0 || WARN_ON_ONCE(!tp->prior_cwnd)) return;' which uses short-circuit || evaluation, guaranteeing early return when tp->prior_cwnd is zero.
- The short-circuit evaluation of || ensures the WARN_ON_ONCE(!tp->prior_cwnd) check is always evaluated before any division by tp->prior_cwnd occurs, since the return statement immediately follows the guard.
- tcp_init_cwnd_reduction sets tp->prior_cwnd = tp->snd_cwnd, establishing that prior_cwnd reflects the congestion window value at the time congestion control state is initialized.

Invariant verification:
- Division by zero prevention for tp->prior_cwnd: holds=true. Evidence: The guard 'if (newly_acked_sacked <= 0 || WARN_ON_ONCE(!tp->prior_cwnd)) return;' at the function entry ensures tp->prior_cwnd != 0 before any subsequent code executes, including the div_u64(dividend, tp->prior_cwnd) call in the delta < 0 branch. The || operator's short-circuit semantics guarantee the check is evaluated and the return is taken if prior_cwnd is zero.
- All code paths reaching division pass the non-zero guard: holds=true. Evidence: The function has a single entry point and the guard is placed before all computational branches. The delta < 0 branch containing div_u64(dividend, tp->prior_cwnd) can only be reached after the guard has confirmed tp->prior_cwnd is non-zero.

Residual risk notes: The safety of the division relies on tp->prior_cwnd remaining non-zero between the guard check and the div_u64 call. If concurrent modification of tp->prior_cwnd were possible, a TOCTOU issue could arise. However, the surrounding TCP input processing context typically holds the socket lock (sk_lock), serializing access to these fields. Without visibility into the locking discipline in the callers, this assumption cannot be fully verified from the supplied context alone.

## Analysis

### Function purpose
`tcp_cwnd_reduction()` implements Proportional Rate Reduction (PRR) per RFC 6937. It computes how many packets to send (`sndcnt`) during loss recovery and updates `snd_cwnd` accordingly.

### Data flow review

1. `delta = tp->snd_ssthresh - tcp_packets_in_flight(tp)` — signed `int`. Both operands are `u32`; the difference is stored in a signed `int`, deliberately, so the `delta < 0` branch is meaningful.

2. `newly_acked_sacked = prior_unsacked - (tp->packets_out - tp->sacked_out)` — also signed; early exit if `<= 0`.

3. The early return also bails out via `WARN_ON_ONCE(!tp->prior_cwnd)` — guards against div-by-zero in `div_u64(dividend, tp->prior_cwnd)`. `tp->prior_cwnd` is set in `tcp_init_cwnd_reduction()` from `tp->snd_cwnd`, which TCP keeps `>= 1` (TCP_INIT_CWND etc.), so the WARN path is essentially defense in depth.

4. Dividend computation:
   - `dividend = (u64)tp->snd_ssthresh * tp->prr_delivered + tp->prior_cwnd - 1`
   - `snd_ssthresh` and `prr_delivered` are `u32`, multiplication promoted to `u64` — no overflow concern (max ~2^64).
   - The `+ prior_cwnd - 1` is the standard ceiling-division trick.

5. `sndcnt = div_u64(dividend, tp->prior_cwnd) - tp->prr_out;` — `div_u64` returns `u64`; assigned to `int sndcnt`. The PRR algorithm guarantees the quotient does not grossly exceed `prr_out + cwnd`, so this is bounded in practice. Could theoretically wrap if `prr_out` grows faster than the quotient — but that violates PRR invariants; `prr_out` is incremented only when packets are sent and the quotient grows by `ssthresh/prior_cwnd` per delivered packet.

6. Final: `tp->snd_cwnd = tcp_packets_in_flight(tp) + sndcnt;` — `snd_cwnd` is `u32`. If `sndcnt` were negative, this could shrink cwnd below valid range, but later TCP code clamps cwnd before use.

### Guards present
- Early return on non-positive `newly_acked_sacked`.
- WARN + return on zero `prior_cwnd` (prevents division by zero).
- Use of `u64` arithmetic for the multiplication.
- `max(sndcnt, fast_rexmit ? 1 : 0)` ensures non-negative sndcnt when fast retransmit is active.

### Potential concerns
- When `delta < 0` and `prr_out` is large, `sndcnt = quotient - prr_out` could be negative. It's then clamped only by `max(sndcnt, fast_rexmit ? 1 : 0)`, so when `fast_rexmit == 0`, a negative `sndcnt` is possible. Adding it to `tcp_packets_in_flight(tp)` (a `u32`) could result in a small/wrapped `snd_cwnd`. However, this is the intended PRR behavior (reducing cwnd toward in-flight) and TCP transmission paths re-clamp cwnd; it isn't a memory safety issue.
- No attacker-controlled buffer indexing, no memory writes beyond `tp` struct fields, no length used for copy operations.

### Attacker model
Inputs (`prior_unsacked`, `flag`) come from ACK processing. A remote peer can influence `sacked_out`, `packets_out`, `prr_delivered` indirectly. The worst observable effect is incorrect cwnd computation (a congestion-control fairness/throughput issue), not memory corruption or privilege escalation.

### Verdict

**Not a security vulnerability.** The function is congestion-control arithmetic with proper `u64` promotion for the multiplication, an explicit divide-by-zero guard via `WARN_ON_ONCE(!tp->prior_cwnd)`, and an early return on degenerate `newly_acked_sacked`. The remaining concerns (potential transient negative `sndcnt` leading to a small/underflowed `snd_cwnd`) are correctness/performance issues at most and are bounded by other cwnd-clamping logic on the transmit path. There is no buffer overflow, no untrusted pointer, no race, and no division by zero reachable from the shown code.

Residual uncertainty: full PRR invariants depend on caller-side accounting (`prr_out`, `prr_delivered` updates elsewhere). Nothing in the supplied context contradicts those invariants, and even if violated the impact is limited to congestion control behavior.

Classification: **No vulnerability** (benign congestion-control arithmetic with adequate guards).

Not exploitable based on supplied code. Inputs derive from ACK processing; the function performs congestion-control arithmetic only and does not perform pointer/buffer operations driven by these values.

None established. A remote peer can influence ACK-driven counters but cannot induce memory corruption through this routine.

At worst, transient incorrect snd_cwnd computation (congestion-control correctness/performance), not memory safety or privilege impact.

The function explicitly guards division by zero via WARN_ON_ONCE(!tp->prior_cwnd) with early return. The multiplication tp->snd_ssthresh * tp->prr_delivered is promoted to u64, preventing 32-bit overflow. An early return handles non-positive newly_acked_sacked. Resulting snd_cwnd values are clamped/validated on the transmit path. No buffer indexing, copy length, or attacker-controlled pointer is involved.

prior_cwnd is non-zero before use as a divisor (enforced by WARN_ON_ONCE early return; initialized from snd_cwnd which TCP keeps >=1).

Multiplication of u32 operands is performed in u64 to avoid overflow.

newly_acked_sacked > 0 before any state update.

sndcnt is forced >= 1 when fast_rexmit is set.

Verified from the shown code: explicit WARN_ON_ONCE on !prior_cwnd guards div_u64; (u64) cast on snd_ssthresh promotes the multiply; the early return handles newly_acked_sacked<=0; max(sndcnt, fast_rexmit?1:0) enforces minimum during fast retransmit.

PRR correctness depends on prr_out/prr_delivered accounting elsewhere; if those invariants were violated, sndcnt could be negative producing a shrunken snd_cwnd, but this is bounded by transmit-path clamping and is a performance, not security, concern.

tcp_enter_cwr → tcp_init_cwnd_reduction → tcp_cwnd_reduction → tcp_try_keep_open → tcp_process_loss → tcp_fastretrans_alert