A batteries included HTTP/1.1 client in OCaml
at main 81 lines 3.5 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** Redirect handling and cross-origin security utilities 7 8 This module provides shared functions for handling HTTP redirects safely, 9 including cross-origin detection and sensitive header stripping. *) 10 11let src = Logs.Src.create "requests.redirect" ~doc:"HTTP Redirect Handling" 12module Log = (val Logs.src_log src : Logs.LOG) 13 14(** {1 Cross-Origin Detection} *) 15 16(** Get the effective port for a URI, using default ports for http/https. 17 Per RFC 6454, the port is part of the origin tuple. *) 18let effective_port uri = 19 match Uri.port uri with 20 | Some p -> p 21 | None -> 22 match Uri.scheme uri with 23 | Some "https" -> 443 24 | Some "http" | None -> 80 25 | Some _ -> 80 (* Default for unknown schemes *) 26 27(** Check if two URIs have the same origin for security purposes. 28 Per RFC 6454 (Web Origin), origins are tuples of (scheme, host, port). 29 Used to determine if sensitive headers (Authorization, Cookie) should be 30 stripped during redirects. Following Python requests behavior: 31 - Same host, same scheme, same port = same origin 32 - http -> https upgrade on same host with default ports = allowed (more secure) 33 TODO: Support .netrc for re-acquiring auth credentials on new hosts *) 34let same_origin uri1 uri2 = 35 let host1 = Uri.host uri1 |> Option.map String.lowercase_ascii in 36 let host2 = Uri.host uri2 |> Option.map String.lowercase_ascii in 37 let scheme1 = Uri.scheme uri1 |> Option.value ~default:"http" in 38 let scheme2 = Uri.scheme uri2 |> Option.value ~default:"http" in 39 let port1 = effective_port uri1 in 40 let port2 = effective_port uri2 in 41 match host1, host2 with 42 | Some h1, Some h2 when String.equal h1 h2 -> 43 if String.equal scheme1 scheme2 && port1 = port2 then 44 (* Same scheme, host, and port = same origin *) 45 true 46 else if scheme1 = "http" && scheme2 = "https" && port1 = 80 && port2 = 443 then 47 (* http->https upgrade on default ports is allowed (more secure) *) 48 true 49 else 50 false 51 | _ -> false 52 53(** {1 Sensitive Header Protection} *) 54 55(** Strip sensitive headers for cross-origin redirects to prevent credential leakage. 56 Per Recommendation #1: Also strip Cookie, Proxy-Authorization, WWW-Authenticate *) 57let strip_sensitive_headers headers = 58 headers 59 |> Headers.remove `Authorization 60 |> Headers.remove `Cookie 61 |> Headers.remove `Proxy_authorization 62 |> Headers.remove `Www_authenticate 63 64(** {1 Redirect URL Validation} *) 65 66(** Allowed redirect URL schemes to prevent SSRF attacks. 67 Per Recommendation #5: Only allow http:// and https:// schemes *) 68let allowed_schemes = ["http"; "https"] 69 70(** Validate redirect URL scheme to prevent SSRF attacks. 71 Per Recommendation #5: Only allow http:// and https:// schemes. 72 @raise Error.Invalid_redirect if scheme is not allowed *) 73let validate_url location = 74 let uri = Uri.of_string location in 75 match Uri.scheme uri with 76 | Some scheme when List.mem (String.lowercase_ascii scheme) allowed_schemes -> 77 uri 78 | Some scheme -> 79 raise (Error.invalid_redirectf ~url:location "Disallowed redirect scheme: %s" scheme) 80 | None -> 81 uri (* Relative URLs are OK - they will be resolved against current URL *)