Skip to content

Commit 569d8e2

Browse files
committed
Added scanning filter for common exploit scans
1 parent 3992e2d commit 569d8e2

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@
163163
};
164164
});
165165

166+
app.UseSecurityFilter();
167+
166168
app.MapPost("/backupreports/{token}",
167169
async ([FromServices] IngressHandler handler, [FromRoute] string token, CancellationToken ct) =>
168170
{

SecurityMiddleware.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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)", "&lt;!--#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

Comments
 (0)