From 7e431c388dcb23683d1c54aaa4d782826a256e62 Mon Sep 17 00:00:00 2001 From: nfbot Date: Fri, 15 May 2026 14:07:22 +0100 Subject: [PATCH] Harden WebServer listener and ReadBody - Add guards against zero/overflow content-lengths and catches all exceptions in ReadBody(). Teturning null instead of propagating to the caller. - Wrap StartListner code with try catch blocks. - Each request thread now also catches unhandled exceptions and returns 500. --- .../HttpListenerRequestExtensions.cs | 85 +++++-- nanoFramework.WebServer/WebServer.cs | 216 ++++++++++-------- 2 files changed, 184 insertions(+), 117 deletions(-) diff --git a/nanoFramework.WebServer/HttpListenerRequestExtensions.cs b/nanoFramework.WebServer/HttpListenerRequestExtensions.cs index c9363fb..e33decf 100644 --- a/nanoFramework.WebServer/HttpListenerRequestExtensions.cs +++ b/nanoFramework.WebServer/HttpListenerRequestExtensions.cs @@ -21,45 +21,82 @@ public static MultipartFormDataParser ReadForm(this HttpListenerRequest httpList MultipartFormDataParser.Parse(httpListenerRequest.InputStream); /// - /// Reads a body from the HttpListenerRequest inputstream + /// Reads a body from the HttpListenerRequest inputstream. /// /// The request to read the body from - /// A byte[] containing the body of the request + /// + /// A byte[] containing the body of the request, or if the body could not be read. + /// public static byte[] ReadBody(this HttpListenerRequest httpListenerRequest) { - byte[] body = new byte[httpListenerRequest.ContentLength64]; - byte[] buffer = new byte[4096]; - Stream stream = httpListenerRequest.InputStream; + long contentLength = httpListenerRequest.ContentLength64; - int position = 0; + // check missing or invalid content-length + if (contentLength <= 0) + { + return new byte[0]; + } + + // Sanity check for huge content-length + // A managed array cannot exceed int.MaxValue elements + // Treat an oversized Content-Length the same as an allocation failure. + if (contentLength > int.MaxValue) + { + return null; + } - while (true) + try { - // The stream is (should be) a NetworkStream which might still be receiving data while - // we're already processing. Give the stream a chance to receive more data or we might - // end up with "zero bytes read" too soon... - Thread.Sleep(1); + int bodySize = (int)contentLength; + byte[] body = new byte[bodySize]; + byte[] buffer = new byte[4096]; + Stream stream = httpListenerRequest.InputStream; - long length = stream.Length; + int position = 0; - if (length > buffer.Length) + while (position < bodySize) { - length = buffer.Length; - } + // The stream is (should be) a NetworkStream which might still be receiving data while + // we're already processing. Give the stream a chance to receive more data or we might + // end up with "zero bytes read" too soon... + Thread.Sleep(1); - int bytesRead = stream.Read(buffer, 0, (int)length); + long length = stream.Length; - if (bytesRead == 0) - { - break; - } + if (length <= 0) + { + break; + } - Array.Copy(buffer, 0, body, position, bytesRead); + if (length > buffer.Length) + { + length = buffer.Length; + } - position += bytesRead; - } + long remaining = bodySize - position; + if (length > remaining) + { + length = remaining; + } - return body; + int bytesRead = stream.Read(buffer, 0, (int)length); + + if (bytesRead == 0) + { + break; + } + + Array.Copy(buffer, 0, body, position, bytesRead); + + position += bytesRead; + } + + return body; + } + catch + { + return null; + } } } } diff --git a/nanoFramework.WebServer/WebServer.cs b/nanoFramework.WebServer/WebServer.cs index c25ef68..93756b1 100644 --- a/nanoFramework.WebServer/WebServer.cs +++ b/nanoFramework.WebServer/WebServer.cs @@ -10,6 +10,7 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Security; +using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -664,7 +665,20 @@ private void StartListener() _listener.Start(); while (!_cancel) { - HttpListenerContext context = _listener.GetContext(); + HttpListenerContext context; + + try + { + context = _listener.GetContext(); + } + catch (SocketException) + { + // possibly a transient connection error + // (e.g. OS captive-portal probe reset before request line is received) + // this allows continue and accepting the next connection + continue; + } + if (context == null) { return; @@ -672,143 +686,159 @@ private void StartListener() new Thread(() => { - //This is for handling with transitory or bad requests - if (context.Request.RawUrl == null) + try { - return; - } + //This is for handling with transitory or bad requests + if (context.Request.RawUrl == null) + { + return; + } - CallbackRoutes selectedRoute = null; - bool selectedRouteHasAuth = false; - string multipleCallback = null; - bool hasAuthRoutes = false; - string basicAuthNoCred = null; - bool authFailed = false; + CallbackRoutes selectedRoute = null; + bool selectedRouteHasAuth = false; + string multipleCallback = null; + bool hasAuthRoutes = false; + string basicAuthNoCred = null; + bool authFailed = false; - // Variables used only within the "for". They are here for performance reasons - bool mustAuthenticate; - bool isAuthOk; + // Variables used only within the "for". They are here for performance reasons + bool mustAuthenticate; + bool isAuthOk; - foreach (CallbackRoutes route in _callbackRoutes) - { - if (!IsRouteMatch(route, context.Request.HttpMethod, context.Request.RawUrl)) + foreach (CallbackRoutes route in _callbackRoutes) { - continue; - } + if (!IsRouteMatch(route, context.Request.HttpMethod, context.Request.RawUrl)) + { + continue; + } - // Check auth first - mustAuthenticate = route.Authentication != null && route.Authentication.AuthenticationType != AuthenticationType.None; - if (mustAuthenticate) - { - hasAuthRoutes = true; - if (route.Authentication.AuthenticationType == AuthenticationType.Basic) + // Check auth first + mustAuthenticate = route.Authentication != null && route.Authentication.AuthenticationType != AuthenticationType.None; + if (mustAuthenticate) { - var credReq = context.Request.Credentials; - if (credReq is null) + hasAuthRoutes = true; + if (route.Authentication.AuthenticationType == AuthenticationType.Basic) { - if (basicAuthNoCred is null) + var credReq = context.Request.Credentials; + if (credReq is null) { - basicAuthNoCred = route.Route; + if (basicAuthNoCred is null) + { + basicAuthNoCred = route.Route; + } + + continue; } - continue; + var credSite = route.Authentication.Credentials ?? Credential; + + isAuthOk = credSite != null + && (credSite.UserName == credReq.UserName) + && (credSite.Password == credReq.Password); } + else if (route.Authentication.AuthenticationType == AuthenticationType.ApiKey) + { + var apikeyReq = GetApiKeyFromHeaders(context.Request.Headers); + if (apikeyReq is null) + { + continue; + } - var credSite = route.Authentication.Credentials ?? Credential; + var apikeySite = route.Authentication.ApiKey ?? ApiKey; - isAuthOk = credSite != null - && (credSite.UserName == credReq.UserName) - && (credSite.Password == credReq.Password); - } - else if (route.Authentication.AuthenticationType == AuthenticationType.ApiKey) - { - var apikeyReq = GetApiKeyFromHeaders(context.Request.Headers); - if (apikeyReq is null) + isAuthOk = apikeyReq == apikeySite; + } + else { - continue; + isAuthOk = false; } - var apikeySite = route.Authentication.ApiKey ?? ApiKey; + if (isAuthOk) + { + // This route can be used and has precedence over non-authenticated routes + if (!selectedRouteHasAuth) + { + selectedRoute = null; + multipleCallback = null; + } + + selectedRouteHasAuth = true; + } + else + { + authFailed = true; + continue; + } + } + else if (selectedRouteHasAuth || authFailed) + { + // The selected route has authentication and/or a route exists with failed authentication. + // Those have precedence over non-authenticated routes + continue; + } - isAuthOk = apikeyReq == apikeySite; + if (selectedRoute is null) + { + selectedRoute = route; } else { - isAuthOk = false; + multipleCallback ??= $"Multiple matching callbacks: {selectedRoute.Callback.DeclaringType.FullName}.{selectedRoute.Callback.Name}"; + multipleCallback += $", {route.Callback.DeclaringType.FullName}.{route.Callback.Name}"; } + } + - if (isAuthOk) + if (selectedRoute is null) + { + if (hasAuthRoutes) { - // This route can be used and has precedence over non-authenticated routes - if (!selectedRouteHasAuth) + if (!authFailed && basicAuthNoCred is not null) { - selectedRoute = null; - multipleCallback = null; + context.Response.Headers.Add("WWW-Authenticate", + $"Basic realm=\"Access to {basicAuthNoCred}\""); } - selectedRouteHasAuth = true; + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + context.Response.ContentLength64 = 0; + } + else if (CommandReceived != null) + { + // Starting a new thread to be able to handle a new request in parallel + CommandReceived.Invoke(this, new WebServerEventArgs(context)); } else { - authFailed = true; - continue; + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + context.Response.ContentLength64 = 0; } } - else if (selectedRouteHasAuth || authFailed) - { - // The selected route has authentication and/or a route exists with failed authentication. - // Those have precedence over non-authenticated routes - continue; - } - - if (selectedRoute is null) + else if (multipleCallback is not null) { - selectedRoute = route; + multipleCallback += "."; + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + OutputAsStream(context.Response, multipleCallback); } else { - multipleCallback ??= $"Multiple matching callbacks: {selectedRoute.Callback.DeclaringType.FullName}.{selectedRoute.Callback.Name}"; - multipleCallback += $", {route.Callback.DeclaringType.FullName}.{route.Callback.Name}"; + InvokeRoute(selectedRoute, context); } - } - - if (selectedRoute is null) + HandleContextResponse(context); + } + catch (Exception ex) { - if (hasAuthRoutes) - { - if (!authFailed && basicAuthNoCred is not null) - { - context.Response.Headers.Add("WWW-Authenticate", - $"Basic realm=\"Access to {basicAuthNoCred}\""); - } - context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; - context.Response.ContentLength64 = 0; - } - else if (CommandReceived != null) + try { - // Starting a new thread to be able to handle a new request in parallel - CommandReceived.Invoke(this, new WebServerEventArgs(context)); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.Response.ContentLength64 = 0; } - else + catch { - context.Response.StatusCode = (int)HttpStatusCode.NotFound; - context.Response.ContentLength64 = 0; + // Response may already be partially sent or the connection closed; nothing to do. } } - else if (multipleCallback is not null) - { - multipleCallback += "."; - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - OutputAsStream(context.Response, multipleCallback); - } - else - { - InvokeRoute(selectedRoute, context); - } - - HandleContextResponse(context); }).Start(); }