1+ // Copyright (c) 2025 Duplicati Inc.
2+ //
3+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4+ // of this software and associated documentation files (the "Software"), to deal
5+ // in the Software without restriction, including without limitation the rights to
6+ // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7+ // of the Software, and to permit persons to whom the Software is furnished to do
8+ // so, subject to the following conditions:
9+ //
10+ // The above copyright notice and this permission notice shall be included in all
11+ // copies or substantial portions of the Software.
12+ //
13+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
16+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+ // SOFTWARE.
20+ namespace DuplicatiIngress ;
21+
22+ /// <summary>
23+ /// Middleware to filter against most common security threats and crawlers.
24+ /// This is a simple implementation and is meant to provide basic protection and
25+ /// discourage automated attacks as the requests will be blocked with a 403 status code.
26+ ///
27+ /// This in conjunction with the rate limiting options provides basic protection.
28+ /// </summary>
29+ public class SecurityMiddleware ( RequestDelegate next , ILogger < SecurityMiddleware > logger )
30+ {
31+
32+ /// <summary>
33+ /// These are the file extensions that are blocked by default.
34+ /// </summary>
35+ private static readonly HashSet < string > _blockedExtensions = new ( StringComparer . OrdinalIgnoreCase )
36+ {
37+ ".php" , ".cgi" , ".asp" , ".aspx" , ".ashx" , ".asmx" , ".axd" , ".config" , ".env" ,
38+ ".exe" , ".dll" , ".bat" , ".cmd" , ".sh" , ".jar" , ".jsp" , ".jspx" , ".war" ,
39+ ".pl" , ".py" , ".rb" , ".htaccess" , ".htpasswd" , ".ini" , ".cfg" , ".xml" , ".conf"
40+ } ;
41+
42+ /// <summary>
43+ /// This is a comprehensive list of attack paterns to filter, this is not exhaustive
44+ /// it can be incremented with more patterns as needed.
45+ /// </summary>
46+ private static readonly HashSet < string > _blockedPatterns = new ( StringComparer . OrdinalIgnoreCase )
47+ {
48+ // Path traversal
49+ "../../" , "../" , "..\\ " , "%2e%2e" , "%252e" , "..;" , "%c0%ae" ,
50+
51+ // XSS
52+ "<script" , "javascript:" , "onload=" , "onerror=" , "onmouseover=" ,
53+ "onfocus=" , "onblur=" , "alert(" , "confirm(" , "prompt(" ,
54+ "document.cookie" , "document.domain" , "document.write" ,
55+
56+ // File inclusion/disclosure
57+ ".htaccess" , "etc/passwd" , "win.ini" , "web.config" , ".env" ,
58+ "wp-config" , "config.php" , "phpinfo" , ".git/" , ".svn/" ,
59+
60+ // Command injection
61+ "; ls" , "; dir" , "|ls" , "|dir" , "&&ls" , "&&dir" , "||ls" , "||dir" ,
62+ "`ls`" , "`dir`" , "$(ls)" , "$(dir)" , "<!--#exec" ,
63+
64+ // NoSQL injection
65+ "$where:" , "$gt:" , "$lt:" , "$ne:" , "$in:" , "$regex:" ,
66+
67+ // Template injection
68+ "{{" , "${" , "#{" , "<%= " , "[% " , "<? " , "<%"
69+ } ;
70+
71+ /// <inheritdoc />
72+ public async Task InvokeAsync ( HttpContext context )
73+ {
74+ if ( IsBlocked ( context ) )
75+ {
76+ var remoteip = context . Request . Headers [ "X-Forwarded-For" ] . FirstOrDefault ( ) ?? context . Connection . RemoteIpAddress ? . ToString ( ) ?? "unknown" ;
77+ logger . LogWarning ( "Blocked request from IP {IP} with path {Path} and user agent {UserAgent}" ,
78+ remoteip ,
79+ context . Request . Path ,
80+ context . Request . Headers . UserAgent . ToString ( ) ) ;
81+
82+ context . Response . StatusCode = StatusCodes . Status403Forbidden ;
83+ return ;
84+ }
85+
86+ await next ( context ) ;
87+ }
88+
89+ /// <summary>
90+ /// Returns a requests matches any of the blocked patterns or extensions.
91+ /// </summary>
92+ /// <param name="context">Context of the request</param>
93+ /// <returns>True if the request should be blocked</returns>
94+ private bool IsBlocked ( HttpContext context )
95+ {
96+ var path = context . Request . Path . ToString ( ) . ToLowerInvariant ( ) ;
97+
98+ // Check file extensions
99+ if ( _blockedExtensions . Any ( x => path . EndsWith ( x ) ) )
100+ return true ;
101+
102+ // Check for suspicious patterns in path, query string, and headers
103+ var valuesToCheck = new [ ]
104+ {
105+ path ,
106+ context . Request . QueryString . ToString ( ) . ToLowerInvariant ( ) ,
107+ context . Request . Headers [ "Referer" ] . ToString ( ) ,
108+ context . Request . Headers [ "Cookie" ] . ToString ( ) ,
109+ context . Request . Headers [ "X-Forwarded-For" ] . ToString ( ) ,
110+ context . Request . Headers [ "X-Forwarded-Host" ] . ToString ( )
111+ } ;
112+
113+ return valuesToCheck . Any ( value =>
114+ _blockedPatterns . Any ( pattern => value . Contains ( pattern , StringComparison . OrdinalIgnoreCase ) ) ) ;
115+ }
116+ }
117+
118+ /// <summary>
119+ /// Helper to register the middleware on app.
120+ /// </summary>
121+ public static class SecurityMiddlewareExtensions
122+ {
123+ /// <summary>
124+ /// Use the security filter middleware
125+ /// </summary>
126+ /// <param name="builder">The application builder</param>
127+ /// <returns>The application builder</returns>
128+ public static IApplicationBuilder UseSecurityFilter ( this IApplicationBuilder builder )
129+ => builder . UseMiddleware < SecurityMiddleware > ( ) ;
130+ }
0 commit comments