Validation & Security

Learn how to validate input, implement CORS, authentication, rate limiting, and security headers.

Basic Input Validation

Validate request data before processing:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.RequestErrors

let validateEmail (email: string) =
  email.Contains("@") && email.Contains(".")

let app =
  choose [
    POST >=> path "/subscribe" >=> fun ctx ->
      match ctx.request.formData "email" with
      | Choice1Of2 email when validateEmail email ->
          OK (sprintf "Subscribed: %s" email) ctx
      | Choice1Of2 _ ->
          BAD_REQUEST "Invalid email address" ctx
      | Choice2Of2 _ ->
          BAD_REQUEST "Missing email field" ctx
  ]

CORS (Cross-Origin Resource Sharing)

Enable CORS for specific domains:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.CORS

let corsConfig = 
  { defaultCORSConfig with 
      allowedUris = InclusiveOption.Some ["http://localhost:3000"; "https://example.com"] }

let app =
  choose [
    GET >=> path "/api/data" >=> cors corsConfig >=> OK "CORS enabled"
  ]

Security Headers

Add important security headers to responses:

open Suave
open Suave.Operators
open Suave.Writers
open Suave.Filters
open Suave.Successful

let setSecurityHeaders =
  setHeader "X-Content-Type-Options" "nosniff" >=>
  setHeader "X-Frame-Options" "SAMEORIGIN" >=>
  setHeader "X-XSS-Protection" "1; mode=block" >=>
  setHeader "Strict-Transport-Security" "max-age=31536000; includeSubDomains"

let app =
  choose [
    path "/" >=> setSecurityHeaders >=> OK "Secure content"
  ]

Basic Authentication

Implement basic HTTP authentication:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.Authentication

let check (username, password) =
  username = "admin" && password = "secret"

let app =
  choose [
    GET >=> path "/admin" >=> Authentication.authenticateBasic check (OK "Admin area")
  ]

Rate Limiting

Implement rate limiting to protect against abuse:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.RateLimit

// Simple rate limit: 10 requests per minute
let rateLimit10PerMin =
  rateLimit (perMinute 10) (OK "Request successful")

// By user (using header)
let limitByUser =
  rateLimit 
    (perMinute 100 
     |> withKeyExtractor (headerKeyExtractor "X-User-Id"))
    (OK "User request successful")

let app =
  choose [
    path "/api/public" >=> rateLimit10PerMin
    path "/api/private" >=> limitByUser
  ]

Request Size Limits

Limit request body size to prevent abuse:

open Suave
open Suave.Successful

let config =
  { defaultConfig with
      bindings = [ HttpBinding.createSimple HTTP "0.0.0.0" 8080 ]
      maxContentLength = 1024 * 1024  // 1 MB limit
  }

[<EntryPoint>]
let main argv =
  startWebServer config (OK "Size limited")
  0

Listen / bind timeout

listenTimeout controls how long Suave waits for the socket bind to finish (not per-request duration):

open Suave
open System
open Suave.Successful

let config =
  { defaultConfig with
      bindings = [ HttpBinding.createSimple HTTP "0.0.0.0" 8080 ]
      listenTimeout = TimeSpan.FromSeconds(30.0)
  }

[<EntryPoint>]
let main argv =
  startWebServer config (OK "Configured")
  0

Input Sanitization

Sanitize user input to prevent injection attacks:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful

open System.Net
open Suave.RequestErrors

let sanitize (input: string) =
  WebUtility.HtmlEncode input

let app =
  choose [
    POST >=> path "/comment" >=> request (fun r ->
      match r.formData "text" with
      | Choice1Of2 text ->
          OK (sprintf "Comment: %s" (sanitize text))
      | Choice2Of2 _ ->
          RequestErrors.BAD_REQUEST "Missing text")
  ]

HTTPS Only

Enforce HTTPS and redirect HTTP requests:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.Redirection

let enforceHttps : WebPart =
  fun ctx ->
    if ctx.request.binding.scheme.secure then
      async { return Some ctx }
    else
      let url = "https://" + ctx.request.host + ctx.request.path
      FOUND url ctx

let app =
  choose [
    enforceHttps >=> path "/" >=> OK "Secure connection"
  ]

Request Validation Pattern

Complete validation pattern for common scenarios:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.RequestErrors

type CreateUserRequest = { name: string; email: string; age: int }

let validateUser (req: CreateUserRequest) =
  match req with
  | _ when req.name.Length < 2 -> Result.Error "Name too short"
  | _ when not (req.email.Contains "@") -> Result.Error "Invalid email"
  | _ when req.age < 18 -> Result.Error "Must be 18+"
  | _ -> Result.Ok req

let app =
  choose [
    POST >=> path "/users" >=> fun ctx ->
      async {
        try
          // rawForm is already a byte[]
          let body = ctx.request.rawForm
          let json = System.Text.Encoding.UTF8.GetString(body)
          let req = System.Text.Json.JsonSerializer.Deserialize<CreateUserRequest>(json)
          
          match validateUser req with
          | Result.Ok validReq -> 
              return! OK (sprintf "User created: %s" validReq.name) ctx
          | Result.Error msg -> 
              return! BAD_REQUEST msg ctx
        with _ ->
          return! BAD_REQUEST "Invalid request" ctx
      }
  ]