From b3f9ed789d64892519b3a60fbca775652e067f45 Mon Sep 17 00:00:00 2001 From: Andrew Meier Date: Tue, 24 Mar 2026 19:05:14 -0400 Subject: [PATCH] MEIER-352: Fix WAF case-sensitive bypass and switch Serilog to OTLP sink --- app/src/App/paket.references | 1 - app/src/App/src/Program.fs | 11 +++--- pulumi/src/cloudflare/waf.ts | 75 +++++++++++++++++++++--------------- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/app/src/App/paket.references b/app/src/App/paket.references index 4273eca..1e06cc7 100644 --- a/app/src/App/paket.references +++ b/app/src/App/paket.references @@ -3,7 +3,6 @@ Giraffe Markdig Serilog Serilog.AspNetCore -Serilog.Sinks.Seq Serilog.Sinks.Console Serilog.Sinks.OpenTelemetry Starfederation.Datastar diff --git a/app/src/App/src/Program.fs b/app/src/App/src/Program.fs index 5e45303..fd96f41 100644 --- a/app/src/App/src/Program.fs +++ b/app/src/App/src/Program.fs @@ -9,8 +9,8 @@ open OpenTelemetry.Exporter open OpenTelemetry.Resources open OpenTelemetry.Trace open Serilog -open Serilog.Core open Serilog.Events +open Serilog.Sinks.OpenTelemetry open StarFederation.Datastar.DependencyInjection open System @@ -33,14 +33,15 @@ let configureLogger (config: Config) = if config.debug then LogEventLevel.Debug else LogEventLevel.Information - let levelSwitch = LoggingLevelSwitch(initialLogLevel) - let logger = LoggerConfiguration() - .MinimumLevel.ControlledBy(levelSwitch) + .MinimumLevel.Is(initialLogLevel) .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .WriteTo.Console() - .WriteTo.Seq(serverUrl = config.seq.endpoint, controlLevelSwitch = levelSwitch) + .WriteTo.OpenTelemetry(fun opts -> + opts.Endpoint <- config.seq.endpoint + "/ingest/otlp/v1/logs" + opts.Protocol <- OtlpProtocol.HttpProtobuf + opts.ResourceAttributes <- dict [ "service.name", box config.appName ]) .CreateLogger() Log.Logger <- logger diff --git a/pulumi/src/cloudflare/waf.ts b/pulumi/src/cloudflare/waf.ts index cefd6ab..757637d 100644 --- a/pulumi/src/cloudflare/waf.ts +++ b/pulumi/src/cloudflare/waf.ts @@ -3,48 +3,59 @@ import { provider } from './provider' import { andymeierZone } from './zone' import * as config from '../config' +// Use lower(url_decode()) to normalize both case and percent-encoding. +// Scanners use mixed case (e.g. /ReAcT/.EnV) and URL encoding (e.g. %2F) +// to bypass WAF rules. Cloudflare does NOT decode reserved chars like %2F +// during standard normalization, so url_decode() is required. +const p = 'lower(url_decode(http.request.uri.path))' + const expression = [ // Sensitive dotfiles and directories - '(http.request.uri.path contains "/.env")', - '(http.request.uri.path contains "/.git")', - '(http.request.uri.path contains "/.aws")', - '(http.request.uri.path contains "/.ssh")', - '(http.request.uri.path contains "/.terraform")', + `(${p} contains "/.env")`, + `(${p} contains "/.git")`, + `(${p} contains "/.aws")`, + `(${p} contains "/.ssh")`, + `(${p} contains "/.terraform")`, // CMS and framework probes - '(http.request.uri.path contains "/wp-")', - '(http.request.uri.path contains "/wordpress")', - '(http.request.uri.path contains "/xmlrpc")', - '(http.request.uri.path contains "/phpMyAdmin")', - '(http.request.uri.path contains "/phpmyadmin")', - '(http.request.uri.path contains "/pma")', + `(${p} contains "/wp-")`, + `(${p} contains "/wordpress")`, + `(${p} contains "/xmlrpc")`, + `(${p} contains "/phpmyadmin")`, + `(${p} contains "/pma")`, // Admin and server management - '(http.request.uri.path contains "/admin")', - '(http.request.uri.path contains "/cgi-bin")', - '(http.request.uri.path contains "/actuator")', - '(http.request.uri.path contains "/solr")', - '(http.request.uri.path contains "/telescope")', - '(http.request.uri.path contains "/vendor")', - '(http.request.uri.path contains "/invoker")', - '(http.request.uri.path contains "/balancer-manager")', + `(${p} contains "/admin")`, + `(${p} contains "/cgi-bin")`, + `(${p} contains "/actuator")`, + `(${p} contains "/solr")`, + `(${p} contains "/telescope")`, + `(${p} contains "/vendor")`, + `(${p} contains "/invoker")`, + `(${p} contains "/balancer-manager")`, + `(${p} contains "/login")`, // Credential and config probes - '(http.request.uri.path contains "/credentials")', - '(http.request.uri.path contains "/known_hosts")', - '(http.request.uri.path contains "sendgrid")', - '(http.request.uri.path contains "codecommit")', - '(http.request.uri.path contains "/env.cfg")', + `(${p} contains "/credentials")`, + `(${p} contains "/known_hosts")`, + `(${p} contains "sendgrid")`, + `(${p} contains "codecommit")`, + `(${p} contains "/env.cfg")`, + `(${p} contains "/api/config")`, + `(${p} contains "/careers_not_hosted")`, // Dangerous file extensions - '(http.request.uri.path contains ".php")', - '(http.request.uri.path contains ".asp")', - '(http.request.uri.path contains ".jsp")', - '(http.request.uri.path contains ".cgi")', - '(http.request.uri.path contains ".yml")', - '(http.request.uri.path contains ".xml")', - '(http.request.uri.path contains ".bak")', - '(http.request.uri.path contains ".rb")', + `(${p} contains ".php")`, + `(${p} contains ".asp")`, + `(${p} contains ".jsp")`, + `(${p} contains ".cgi")`, + `(${p} contains ".yml")`, + `(${p} contains ".xml")`, + `(${p} contains ".bak")`, + `(${p} contains ".rb")`, + + // Env file variants (with dots in name like .env.sample, .env.prod) + `(${p} contains ".env.")`, ].join(' or ') new cloudflare.Ruleset(`${config.identifier}-waf`, {