├── tests └── AspNetCore.ReferrerBlock.Tests │ ├── MSTestSettings.cs │ ├── AspNetCore.ReferrerBlock.Tests.csproj │ └── ReferrerBlockMiddlewareTests.cs ├── src └── AspNetCore.ReferrerBlock │ ├── ReferrerBlock.png │ ├── Extensions │ └── ReferrerBlockMiddlewareExtensions.cs │ ├── AspNetCore.ReferrerBlock.csproj │ ├── Configuration │ └── ReferrerBlockOptions.cs │ └── Middleware │ └── ReferrerBlockMiddleware.cs ├── AspNetCore.ReferrerBlock.slnx ├── LICENSE ├── README.md ├── AspNetCore.ReferrerBlock.sln ├── BLOCKED_DOMAINS.md └── .gitignore /tests/AspNetCore.ReferrerBlock.Tests/MSTestSettings.cs: -------------------------------------------------------------------------------- 1 | [assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] 2 | -------------------------------------------------------------------------------- /src/AspNetCore.ReferrerBlock/ReferrerBlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tossnet/AspNetCore.ReferrerBlock/main/src/AspNetCore.ReferrerBlock/ReferrerBlock.png -------------------------------------------------------------------------------- /AspNetCore.ReferrerBlock.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/AspNetCore.ReferrerBlock.Tests/AspNetCore.ReferrerBlock.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | latest 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/AspNetCore.ReferrerBlock/Extensions/ReferrerBlockMiddlewareExtensions.cs: -------------------------------------------------------------------------------- 1 | using ReferrerBlock.Configuration; 2 | using ReferrerBlock.Middleware; 3 | 4 | namespace Microsoft.AspNetCore.Builder; 5 | 6 | public static class ReferrerBlockMiddlewareExtensions 7 | { 8 | public static IApplicationBuilder UseReferrerBlock( 9 | this IApplicationBuilder app, 10 | Action? configure = null) 11 | { 12 | var options = new ReferrerBlockOptions(); 13 | configure?.Invoke(options); 14 | 15 | // Remove null or empty values from collections to prevent blocking legitimate traffic 16 | options.BlockedDomains?.RemoveWhere(string.IsNullOrWhiteSpace); 17 | options.BlockedTLDs?.RemoveWhere(string.IsNullOrWhiteSpace); 18 | options.BlockedPatterns?.RemoveWhere(string.IsNullOrWhiteSpace); 19 | 20 | return app.UseMiddleware(options); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Christophe Peugnet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/AspNetCore.ReferrerBlock/AspNetCore.ReferrerBlock.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | ReferrerBlock 9 | 2025.12.14 10 | Christophe Peugnet 11 | 12 | ReferrerBlock middleware to block referrer spam and malicious traffic based on TLDs, domains, and patterns. Perfect for Blazor, MVC, and Razor Pages applications. 13 | aspnetcore;middleware;security;spam;referrer;anti-spam 14 | https://github.com/tossnet/AspNetCore.ReferrerBlock.git 15 | https://github.com/tossnet/AspNetCore.ReferrerBlock.git 16 | git 17 | MIT 18 | README.md 19 | Initial release with referrer blocking capabilities. 20 | true 21 | true 22 | true 23 | snupkg 24 | 2025 25 | ReferrerBlock.png 26 | True 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | True 46 | \ 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![ReferrerBlock](https://github.com/tossnet/AspNetCore.ReferrerBlock/blob/main/src/AspNetCore.ReferrerBlock/ReferrerBlock.png) 2 | 3 | # ReferrerBlock 4 | 5 | [![NuGet](https://img.shields.io/nuget/v/ReferrerBlock.svg)](https://www.nuget.org/packages/ReferrerBlock/) ![BlazorWinOld Nuget Package](https://img.shields.io/nuget/dt/ReferrerBlock) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | 8 | ReferrerBlock middleware to block referrer spam and malicious traffic. 9 | 10 | ## ⚙️ Usage 11 | ```csharp 12 | var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); 13 | 14 | app.UseReferrerBlock(); 15 | 16 | app.Run(); 17 | ``` 18 | 19 | The middleware uses default blocking rules. Optionally, you can customize them: 20 | ```csharp 21 | app.UseReferrerBlock(options => { 22 | options.BlockedDomains.Add("spam-site.com"); 23 | options.BlockedTLDs.Add(".suspicious"); 24 | options.BlockedPatterns.Add("malicious"); 25 | options.BlockedSubdomainPrefixes.Add("spam"); 26 | }); 27 | ``` 28 | 29 | ## 📝 Examples 30 | 31 | ### Disable default TLD blocking 32 | ```csharp 33 | app.UseReferrerBlock(options => 34 | { 35 | options.BlockedTLDs.Clear(); // Remove all default TLDs 36 | options.BlockedDomains.Add("spam-site.com"); 37 | }); 38 | ``` 39 | 40 | ### Use only custom rules 41 | ```csharp 42 | app.UseReferrerBlock(options => 43 | { 44 | // Clear all default rules 45 | options.BlockedTLDs.Clear(); 46 | options.BlockedDomains.Clear(); 47 | options.BlockedPatterns.Clear(); 48 | options.BlockedSubdomainPrefixes.Clear(); 49 | 50 | // Add only your custom rules 51 | options.BlockedDomains.Add("spam-site.com"); 52 | options.BlockedDomains.Add("malicious-domain.com"); 53 | options.BlockedTLDs.Add(".scam"); 54 | options.BlockedPatterns.Add("suspicious"); 55 | options.BlockedSubdomainPrefixes.Add("bot"); 56 | }); 57 | ``` 58 | 59 | ### Combine default rules with custom ones 60 | ```csharp 61 | app.UseReferrerBlock(options => 62 | { 63 | // Keep default rules and add custom ones 64 | options.BlockedDomains.Add("spam-site.com"); 65 | options.BlockedTLDs.Add(".suspicious"); 66 | options.BlockedPatterns.Add("malicious"); 67 | options.BlockedSubdomainPrefixes.Add("bot"); 68 | }); 69 | ``` 70 | 71 | ### Block subdomain prefixes with numeric variations 72 | The `BlockedSubdomainPrefixes` option allows you to block subdomains that start with a specific prefix followed by optional digits. 73 | 74 | ```csharp 75 | app.UseReferrerBlock(options => 76 | { 77 | // Block subdomains like: iqri., iqri1., iqri18., hk., hk1., hk12., etc. 78 | options.BlockedSubdomainPrefixes.Add("iqri"); 79 | options.BlockedSubdomainPrefixes.Add("hk"); 80 | options.BlockedSubdomainPrefixes.Add("spam"); 81 | }); 82 | ``` 83 | 84 | This will block referrers like: 85 | - `iqri.example.com` ✅ blocked 86 | - `iqri1.spammer.net` ✅ blocked 87 | - `iqri18.malicious.org` ✅ blocked 88 | - `hk12.badsite.com` ✅ blocked 89 | 90 | But will NOT block: 91 | - `iqri1x.example.com` ❌ not blocked (has letters after digits) 92 | - `myiqri1.example.com` ❌ not blocked (prefix not at start) 93 | - `iqrisite.com` ❌ not blocked (in domain name, not subdomain) 94 | 95 | ## 📊 Blocked Domains 96 | 97 | See [BLOCKED_DOMAINS.md](BLOCKED_DOMAINS.md) for the complete list of blocked domains, TLDs, and patterns with their addition history. 98 | -------------------------------------------------------------------------------- /AspNetCore.ReferrerBlock.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore.ReferrerBlock", "src\AspNetCore.ReferrerBlock\AspNetCore.ReferrerBlock.csproj", "{9CBE5E87-B1AE-4C00-A298-721A1C5DA248}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore.ReferrerBlock.Tests", "tests\AspNetCore.ReferrerBlock.Tests\AspNetCore.ReferrerBlock.Tests.csproj", "{4B002A73-5441-4E25-B900-4C036DF3B717}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Debug|x64.ActiveCfg = Debug|Any CPU 27 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Debug|x64.Build.0 = Debug|Any CPU 28 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Debug|x86.ActiveCfg = Debug|Any CPU 29 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Debug|x86.Build.0 = Debug|Any CPU 30 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Release|x64.ActiveCfg = Release|Any CPU 33 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Release|x64.Build.0 = Release|Any CPU 34 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Release|x86.ActiveCfg = Release|Any CPU 35 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248}.Release|x86.Build.0 = Release|Any CPU 36 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Debug|x64.ActiveCfg = Debug|Any CPU 39 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Debug|x64.Build.0 = Debug|Any CPU 40 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Debug|x86.ActiveCfg = Debug|Any CPU 41 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Debug|x86.Build.0 = Debug|Any CPU 42 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Release|x64.ActiveCfg = Release|Any CPU 45 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Release|x64.Build.0 = Release|Any CPU 46 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Release|x86.ActiveCfg = Release|Any CPU 47 | {4B002A73-5441-4E25-B900-4C036DF3B717}.Release|x86.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {9CBE5E87-B1AE-4C00-A298-721A1C5DA248} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} 54 | {4B002A73-5441-4E25-B900-4C036DF3B717} = {0AB3BF05-4346-4AA6-1389-037BE0695223} 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /src/AspNetCore.ReferrerBlock/Configuration/ReferrerBlockOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ReferrerBlock.Configuration; 2 | 3 | public class ReferrerBlockOptions 4 | { 5 | // List of blocked domain extensions 6 | public HashSet BlockedTLDs { get; set; } = new(StringComparer.OrdinalIgnoreCase) 7 | { 8 | ".biz.id", 9 | ".co.id", 10 | ".icu", 11 | ".in", 12 | ".xyz", 13 | }; 14 | 15 | // List of blocked subdomain prefixes (e.g., "iqri" matches iqri., iqri1., iqri18., etc.) 16 | public HashSet BlockedSubdomainPrefixes { get; set; } = new(StringComparer.OrdinalIgnoreCase) 17 | { 18 | "iqri", 19 | "iqra", 20 | "hk", 21 | }; 22 | 23 | // List of suspicious domain patterns (incomplete or generic domains) 24 | public HashSet BlockedPatterns { get; set; } = new(StringComparer.OrdinalIgnoreCase) 25 | { 26 | "cekitchenware", 27 | "crmlogichub", 28 | "ctysss", 29 | "followpathcrm", 30 | "hiplay704", 31 | "ikancupang", 32 | "jbuif", 33 | "karyapemuda", 34 | "kawruhbasa", 35 | "kaysdogs", 36 | "missywilkinson", 37 | "parakerja", 38 | "pvdwatersports", 39 | "pulbarholdings", 40 | "raymandservice", 41 | "rororush", 42 | "sscgdpracticeset", 43 | "sharpdrivers", 44 | "teknovidia", 45 | "tempatbelajar", 46 | "toworld123", 47 | }; 48 | 49 | // List of blocked complete domains 50 | public HashSet BlockedDomains { get; set; } = new(StringComparer.OrdinalIgnoreCase) 51 | { 52 | "airplanesupdate.com", 53 | "aksarabrita.com", 54 | "barondamaluku.com", 55 | "befps.com", 56 | "biayakuliah.id", 57 | "brandipphoto.com", 58 | "chfaloans.com", 59 | "cipicipichips.com", 60 | "crminsightspro.com", 61 | "crmsoftwareedge.com", 62 | "cozynestplans.com", 63 | "def-atk.com", 64 | "dgfzg.com", 65 | "fiveminutesgames.com", 66 | "galaxys21manual.com", 67 | "gazdp.com", 68 | "genshindb.org", 69 | "globalinsuranceresourcecenter.com", 70 | "greenpeakshinecleaning.com", 71 | "hargamakanan.com", 72 | "hellyeahomeland.com", 73 | "holopoems.com", 74 | "indonesiamapan.com", 75 | "jawapos.com", 76 | "jy47.top", 77 | "kebumenupdate.com", 78 | "kiprahkita.com", 79 | "kuyasfoodexpress.com", 80 | "liveasiannews.com", 81 | "lovesyh.com", 82 | "mitsubishiprice.com", 83 | "nativeindonesia.com", 84 | "pengkicau.com", 85 | "periodcostume.top", 86 | "qccaaf.com", 87 | "maibirthday.com", 88 | "mdsdy.com", 89 | "mojok.co", 90 | "mqypc.com", 91 | "needlecorner.com", 92 | "nwgme.online", 93 | "oneadcc.com", 94 | "orderlos3reales1.com", 95 | "pauljimandjoespodcast.com", 96 | "patioinstallationcompanies.com", 97 | "pegawai.info", 98 | "penjudihijrah.com", 99 | "porosjambimedia.com", 100 | "roomvivo.com", 101 | "saastoolcompare.com", 102 | "samsungprinter.app", 103 | "scfxdl.com", 104 | "secondechance.org", 105 | "selectaproperty.us", 106 | "seohost.us", 107 | "smartcrmmanager.com", 108 | "soldatiki.info", 109 | "sportbooksbox.com", 110 | "syrfy.com", 111 | "teknowarta.com", 112 | "thebirdlore.com", 113 | "theguineapigexpert.com", 114 | "topikbisnis.com", 115 | "tipgalore.com", 116 | "tugassains.com", 117 | "trailerhomeleveling.com", 118 | "uaetiming.com", 119 | "vivarecharge.com", 120 | "vtpcys.com", 121 | "wavpn.click", 122 | "wartaupdate.com", 123 | "wagnerdom.local", 124 | "wantmybots.com", 125 | "wellsfederal.com", 126 | "worldhxx.com", 127 | "xlyanghub.com", 128 | "zhengzhouchendi.com", 129 | "zqauc.com", 130 | }; 131 | } 132 | -------------------------------------------------------------------------------- /src/AspNetCore.ReferrerBlock/Middleware/ReferrerBlockMiddleware.cs: -------------------------------------------------------------------------------- 1 | using ReferrerBlock.Configuration; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace ReferrerBlock.Middleware; 6 | 7 | public class ReferrerBlockMiddleware 8 | { 9 | private readonly RequestDelegate _next; 10 | private readonly ILogger _logger; 11 | private readonly ReferrerBlockOptions _options; 12 | 13 | public ReferrerBlockMiddleware( 14 | RequestDelegate next, 15 | ILogger logger, 16 | ReferrerBlockOptions options) 17 | { 18 | _next = next; 19 | _logger = logger; 20 | _options = options; 21 | } 22 | 23 | public async Task InvokeAsync(HttpContext context) 24 | { 25 | var referer = context.Request.Headers.Referer.ToString(); 26 | 27 | if (!string.IsNullOrEmpty(referer)) 28 | { 29 | try 30 | { 31 | // add a schema by defaut is missing 32 | if (!referer.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && 33 | !referer.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) 34 | { 35 | referer = "https://" + referer; 36 | } 37 | 38 | 39 | var uri = new Uri(referer); 40 | var host = uri.Host.ToLowerInvariant(); 41 | 42 | if (IsBlocked(host)) 43 | { 44 | _logger.LogWarning( 45 | "🚫 Referrer spam blocked: {Host} | IP: {IP} | Path: {Path}", 46 | host, 47 | context.Connection.RemoteIpAddress, 48 | context.Request.Path 49 | ); 50 | 51 | // Slow down aggressive bots 52 | await Task.Delay(Random.Shared.Next(100, 500)); 53 | 54 | context.Response.StatusCode = StatusCodes.Status410Gone; 55 | await context.Response.WriteAsync("Gone"); 56 | return; 57 | } 58 | } 59 | catch (UriFormatException) 60 | { 61 | _logger.LogDebug("Malformed referrer detected: {Referer}", referer); 62 | } 63 | } 64 | 65 | await _next(context); 66 | } 67 | 68 | private bool IsBlocked(string host) 69 | { 70 | // Check TLDs - TLDs already contain the dot (e.g., ".icu") 71 | if (_options.BlockedTLDs?.Any(tld => !string.IsNullOrEmpty(tld) && 72 | host.EndsWith(tld, StringComparison.OrdinalIgnoreCase)) == true) 73 | return true; 74 | 75 | // Check exact domains 76 | if (_options.BlockedDomains?.Contains(host, StringComparer.OrdinalIgnoreCase) == true) 77 | return true; 78 | 79 | // Check subdomains 80 | if (_options.BlockedDomains?.Any(blocked => !string.IsNullOrEmpty(blocked) && 81 | host.EndsWith($".{blocked}", StringComparison.OrdinalIgnoreCase)) == true) 82 | return true; 83 | 84 | // Check patterns 85 | if (_options.BlockedPatterns?.Any(pattern => !string.IsNullOrEmpty(pattern) && 86 | host.Contains(pattern, StringComparison.OrdinalIgnoreCase)) == true) 87 | return true; 88 | 89 | // Check subdomain prefixes (e.g., iqri1., hk12.) 90 | if (_options.BlockedSubdomainPrefixes?.Any(prefix => !string.IsNullOrEmpty(prefix) && 91 | IsMatchingSubdomainPrefix(host, prefix)) == true) 92 | return true; 93 | 94 | return false; 95 | } 96 | 97 | /// 98 | /// Checks if host starts with prefix followed by optional digits and a dot. 99 | /// Matches: iqri., iqri1., iqri18. but not iqrix. or iqri1x. 100 | /// 101 | private static bool IsMatchingSubdomainPrefix(ReadOnlySpan host, string prefix) 102 | { 103 | if (!host.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) 104 | return false; 105 | 106 | var remaining = host[prefix.Length..]; 107 | 108 | // Skip any digits after the prefix 109 | int i = 0; 110 | while (i < remaining.Length && char.IsAsciiDigit(remaining[i])) 111 | i++; 112 | 113 | // Must be followed by a dot (subdomain separator) 114 | return i < remaining.Length && remaining[i] == '.'; 115 | } 116 | } -------------------------------------------------------------------------------- /BLOCKED_DOMAINS.md: -------------------------------------------------------------------------------- 1 | # Blocked Domains Registry 2 | 3 | This file documents spam/malicious domains, TLDs, and patterns blocked by default in ReferrerBlock middleware. 4 | 5 | ## Purpose 6 | - **Transparency**: Understand why domains are blocked 7 | - **Maintenance**: Track when domains were added 8 | - **Contribution**: Help reviewers validate new additions 9 | 10 | ## Reporting Issues 11 | If a domain is incorrectly blocked, please [open an issue](https://github.com/tossnet/AspNetCore.ReferrerBlock/issues). 12 | 13 | ## WHOIS Lookup 14 | Use [whois.com](https://www.whois.com/) or similar services to investigate suspicious domains. 15 | 16 | --- 17 | 18 | ## Blocked Items 19 | 20 | | Type | Value | Added | Reason | 21 | |------|-------|-------|--------| 22 | | Domain | (hk n.)airplanesupdate.com | 2025-11-23 | Referrer spam. Multiple subdomain variants) | 23 | | Domain | periodcostume.top | 2025-11-23 | Referrer spam | 24 | | Domain | globalinsuranceresourcecenter.com | 2025-11-23 | Referrer spam | 25 | | Domain | liveasiannews.com | 2025-11-24 | Referrer spam | 26 | | Domain | indonesiamapan.com | 2025-11-24 | Referrer spam | 27 | | Domain | biayakuliah.id | 2025-11-24 | Referrer spam | 28 | | Domain | iklanbarisposkota.top | 2025-11-24 | Referrer spam | 29 | | Domain | chfaloans.com | 2025-11-24 | Referrer spam | 30 | | Domain | wagnerdom.local | 2025-11-25 | Referrer spam | 31 | | Domain | brandipphoto.com | 2025-11-26 | Referrer spam | 32 | | Domain | (glad., eth.)teknowarta.com | 2025-11-26 | Referrer spam. Multiple subdomain variants | 33 | | Domain | def-atk.com | 2025-11-26 | Referrer spam | 34 | | Domain | (tools.)genshindb.org | 2025-11-26 | Referrer spam | 35 | | Domain | hellyeahomeland.com | 2025-11-26 | Referrer spam | 36 | | Domain | petalsearch.com | 2025-11-26 | Referrer spam | 37 | | Domain | (hk n.)seohost.us | 2025-11-27 | Referrer spam. Multiple subdomain variants | 38 | | Domain | (sports.)nativeindonesia.com | 2025-11-27 | Referrer spam | 39 | | Domain | (hk n.)selectaproperty.us | 2025-11-28 | Referrer spam. Multiple subdomain variants | 40 | | Domain | (iqri n.)pauljimandjoespodcast.com | 2025-11-28 | Referrer spam. Multiple subdomain variants | 41 | | Domain | thebirdlore.com | 2025-11-28 | Referrer spam | 42 | | Domain | porosjambimedia.com | 2025-11-28 | Referrer spam | 43 | | Domain | (iqri n.)mitsubishiprice.com | 2025-11-30 | Referrer spam. Multiple subdomain variants | 44 | | Domain | lovesyh.com | 2025-12-01 | Referrer spam | 45 | | Domain | tipgalore.com | 2025-12-01 | Referrer spam | 46 | | Domain | vtpcys.com | 2025-12-02 | Referrer spam | 47 | | Domain | scfxdl.com | 2025-12-03 | Referrer spam | 48 | | Domain | roomvivo.com | 2025-12-03 | Referrer spam | 49 | | Domain | wavpn.click | 2025-12-03 | Referrer spam | 50 | | Domain | crminsightspro.com | 2025-12-05 | Referrer spam | 51 | | Domain | aaruslan.soldatiki.info | 2025-12-05 | Referrer spam | 52 | | Domain | vip.vivarecharge.com | 2025-12-05 | Referrer spam | 53 | | Domain | fintech.syrfy.com | 2025-12-05 | Referrer spam | 54 | | Domain | jy47.top | 2025-12-05 | Referrer spam | 55 | | Domain | refinance.qccaaf.com | 2025-12-05 | Referrer spam | 56 | | Domain | shareit.nwgme.online | 2025-12-05 | Referrer spam | 57 | | Domain | goes.greenpeakshinecleaning.com | 2025-12-06 | Referrer spam | 58 | | Domain | education.jawapos.com | 2025-12-06 | Referrer spam | 59 | | Domain | gs2.trailerhomeleveling.com | 2025-12-06 | Referrer spam | 60 | | Domain | fn.needlecorner.com | 2025-12-06 | Referrer spam | 61 | | Domain | jersey.kuyasfoodexpress.com | 2025-12-06 | Referrer spam | 62 | | Domain | ca.galaxys21manual.com | 2025-12-07 | Referrer spam | 63 | | Domain | dgfzg.com | 2025-12-08 | Referrer spam | 64 | | Domain | guide.zqauc.com | 2025-12-08 | Referrer spam | 65 | | Domain | wantmybots.com | 2025-12-09 | Referrer spam. Multiple subdomain variants | 66 | | Domain | saastoolcompare.com | 2025-12-09 | Referrer spam. | 67 | | Domain | tr.theguineapigexpert.com | 2025-12-10 | Referrer spam. | 68 | | Domain | crmsoftwareedge.com | 2025-12-10 | Referrer spam. | 69 | | Domain | topikbisnis.com | 2025-12-10 | Referrer spam. Multiple subdomain variants | 70 | | Domain | (iqri n.,hk n.)penjudihijrah.com | 2025-12-10 | Referrer spam. Multiple subdomain variants | 71 | | Domain | fiveminutesgames.com | 2025-12-10 | Referrer spam. | 72 | | Domain | s1.oneadcc.com | 2025-12-11 | Referrer spam. | 73 | | Domain | smartcrmmanager.com | 2025-12-11 | Referrer spam. | 74 | | Domain | luxury.maibirthday.com | 2025-12-12 | Referrer spam. | 75 | | Domain | lawyer.mqypc.com | 2025-12-12 | Referrer spam. | 76 | | Domain | dog.worldhxx.com | 2025-12-12 | Referrer spam. | 77 | | Domain | digilife.hargamakanan.com | 2025-12-13 | Referrer spam. | 78 | | Domain | ww51.cipicipichips.com | 2025-12-13 | Referrer spam. | 79 | | Domain | as.pegawai.info | 2025-12-13 | Referrer spam. | 80 | | Domain | one.uaetiming.com | 2025-12-13 | Referrer spam. | 81 | | Domain | mdsdy.com | 2025-12-13 | Referrer spam. | 82 | | Domain | eat.befps.com | 2025-12-13 | Referrer spam. | 83 | | Domain | sl.sportbooksbox.com | 2025-12-13 | Referrer spam. | 84 | | Domain | depe.holopoems.com | 2025-12-13 | Referrer spam. | 85 | | Domain | ag.samsungprinter.app | 2025-12-13 | Referrer spam. | 86 | | Domain | iqra(n). | 2025-12-13 | Subdomain Referrer spam. | 87 | 88 | 89 | 90 | 91 | --- 92 | 93 | ## How to Contribute 94 | 95 | 1. Verify the domain/pattern is actually spam (check analytics, WHOIS) 96 | 2. Add entry to the table with current date 97 | 3. Submit a pull request with evidence/reasoning 98 | 4. Keep entries sorted alphabetically by value within each type 99 | 100 | **Types:** 101 | - **Domain**: Specific domain names (e.g., `spam-site.com`) 102 | - **TLD**: Top-level domains (e.g., `.xyz`, `.top`) 103 | - **Pattern**: Keyword patterns in referrer URLs (e.g., `casino`, `viagra`) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | *.env 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Mono auto generated files 18 | mono_crash.* 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | [Ww][Ii][Nn]32/ 28 | [Aa][Rr][Mm]/ 29 | [Aa][Rr][Mm]64/ 30 | [Aa][Rr][Mm]64[Ee][Cc]/ 31 | bld/ 32 | [Oo]bj/ 33 | [Oo]ut/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Build results on 'Bin' directories 38 | **/[Bb]in/* 39 | # Uncomment if you have tasks that rely on *.refresh files to move binaries 40 | # (https://github.com/github/gitignore/pull/3736) 41 | #!**/[Bb]in/*.refresh 42 | 43 | # Visual Studio 2015/2017 cache/options directory 44 | .vs/ 45 | # Uncomment if you have tasks that create the project's static files in wwwroot 46 | #wwwroot/ 47 | 48 | # Visual Studio 2017 auto generated files 49 | Generated\ Files/ 50 | 51 | # MSTest test Results 52 | [Tt]est[Rr]esult*/ 53 | [Bb]uild[Ll]og.* 54 | *.trx 55 | 56 | # NUnit 57 | *.VisualState.xml 58 | TestResult.xml 59 | nunit-*.xml 60 | 61 | # Approval Tests result files 62 | *.received.* 63 | 64 | # Build Results of an ATL Project 65 | [Dd]ebugPS/ 66 | [Rr]eleasePS/ 67 | dlldata.c 68 | 69 | # Benchmark Results 70 | BenchmarkDotNet.Artifacts/ 71 | 72 | # .NET Core 73 | project.lock.json 74 | project.fragment.lock.json 75 | artifacts/ 76 | 77 | # ASP.NET Scaffolding 78 | ScaffoldingReadMe.txt 79 | 80 | # StyleCop 81 | StyleCopReport.xml 82 | 83 | # Files built by Visual Studio 84 | *_i.c 85 | *_p.c 86 | *_h.h 87 | *.ilk 88 | *.meta 89 | *.obj 90 | *.idb 91 | *.iobj 92 | *.pch 93 | *.pdb 94 | *.ipdb 95 | *.pgc 96 | *.pgd 97 | *.rsp 98 | # but not Directory.Build.rsp, as it configures directory-level build defaults 99 | !Directory.Build.rsp 100 | *.sbr 101 | *.tlb 102 | *.tli 103 | *.tlh 104 | *.tmp 105 | *.tmp_proj 106 | *_wpftmp.csproj 107 | *.log 108 | *.tlog 109 | *.vspscc 110 | *.vssscc 111 | .builds 112 | *.pidb 113 | *.svclog 114 | *.scc 115 | 116 | # Chutzpah Test files 117 | _Chutzpah* 118 | 119 | # Visual C++ cache files 120 | ipch/ 121 | *.aps 122 | *.ncb 123 | *.opendb 124 | *.opensdf 125 | *.sdf 126 | *.cachefile 127 | *.VC.db 128 | *.VC.VC.opendb 129 | 130 | # Visual Studio profiler 131 | *.psess 132 | *.vsp 133 | *.vspx 134 | *.sap 135 | 136 | # Visual Studio Trace Files 137 | *.e2e 138 | 139 | # TFS 2012 Local Workspace 140 | $tf/ 141 | 142 | # Guidance Automation Toolkit 143 | *.gpState 144 | 145 | # ReSharper is a .NET coding add-in 146 | _ReSharper*/ 147 | *.[Rr]e[Ss]harper 148 | *.DotSettings.user 149 | 150 | # TeamCity is a build add-in 151 | _TeamCity* 152 | 153 | # DotCover is a Code Coverage Tool 154 | *.dotCover 155 | 156 | # AxoCover is a Code Coverage Tool 157 | .axoCover/* 158 | !.axoCover/settings.json 159 | 160 | # Coverlet is a free, cross platform Code Coverage Tool 161 | coverage*.json 162 | coverage*.xml 163 | coverage*.info 164 | 165 | # Visual Studio code coverage results 166 | *.coverage 167 | *.coveragexml 168 | 169 | # NCrunch 170 | _NCrunch_* 171 | .NCrunch_* 172 | .*crunch*.local.xml 173 | nCrunchTemp_* 174 | 175 | # MightyMoose 176 | *.mm.* 177 | AutoTest.Net/ 178 | 179 | # Web workbench (sass) 180 | .sass-cache/ 181 | 182 | # Installshield output folder 183 | [Ee]xpress/ 184 | 185 | # DocProject is a documentation generator add-in 186 | DocProject/buildhelp/ 187 | DocProject/Help/*.HxT 188 | DocProject/Help/*.HxC 189 | DocProject/Help/*.hhc 190 | DocProject/Help/*.hhk 191 | DocProject/Help/*.hhp 192 | DocProject/Help/Html2 193 | DocProject/Help/html 194 | 195 | # Click-Once directory 196 | publish/ 197 | 198 | # Publish Web Output 199 | *.[Pp]ublish.xml 200 | *.azurePubxml 201 | # Note: Comment the next line if you want to checkin your web deploy settings, 202 | # but database connection strings (with potential passwords) will be unencrypted 203 | *.pubxml 204 | *.publishproj 205 | 206 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 207 | # checkin your Azure Web App publish settings, but sensitive information contained 208 | # in these scripts will be unencrypted 209 | PublishScripts/ 210 | 211 | # NuGet Packages 212 | *.nupkg 213 | # NuGet Symbol Packages 214 | *.snupkg 215 | # The packages folder can be ignored because of Package Restore 216 | **/[Pp]ackages/* 217 | # except build/, which is used as an MSBuild target. 218 | !**/[Pp]ackages/build/ 219 | # Uncomment if necessary however generally it will be regenerated when needed 220 | #!**/[Pp]ackages/repositories.config 221 | # NuGet v3's project.json files produces more ignorable files 222 | *.nuget.props 223 | *.nuget.targets 224 | 225 | # Microsoft Azure Build Output 226 | csx/ 227 | *.build.csdef 228 | 229 | # Microsoft Azure Emulator 230 | ecf/ 231 | rcf/ 232 | 233 | # Windows Store app package directories and files 234 | AppPackages/ 235 | BundleArtifacts/ 236 | Package.StoreAssociation.xml 237 | _pkginfo.txt 238 | *.appx 239 | *.appxbundle 240 | *.appxupload 241 | 242 | # Visual Studio cache files 243 | # files ending in .cache can be ignored 244 | *.[Cc]ache 245 | # but keep track of directories ending in .cache 246 | !?*.[Cc]ache/ 247 | 248 | # Others 249 | ClientBin/ 250 | ~$* 251 | *~ 252 | *.dbmdl 253 | *.dbproj.schemaview 254 | *.jfm 255 | *.pfx 256 | *.publishsettings 257 | orleans.codegen.cs 258 | 259 | # Including strong name files can present a security risk 260 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 261 | #*.snk 262 | 263 | # Since there are multiple workflows, uncomment next line to ignore bower_components 264 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 265 | #bower_components/ 266 | 267 | # RIA/Silverlight projects 268 | Generated_Code/ 269 | 270 | # Backup & report files from converting an old project file 271 | # to a newer Visual Studio version. Backup files are not needed, 272 | # because we have git ;-) 273 | _UpgradeReport_Files/ 274 | Backup*/ 275 | UpgradeLog*.XML 276 | UpgradeLog*.htm 277 | ServiceFabricBackup/ 278 | *.rptproj.bak 279 | 280 | # SQL Server files 281 | *.mdf 282 | *.ldf 283 | *.ndf 284 | 285 | # Business Intelligence projects 286 | *.rdl.data 287 | *.bim.layout 288 | *.bim_*.settings 289 | *.rptproj.rsuser 290 | *- [Bb]ackup.rdl 291 | *- [Bb]ackup ([0-9]).rdl 292 | *- [Bb]ackup ([0-9][0-9]).rdl 293 | 294 | # Microsoft Fakes 295 | FakesAssemblies/ 296 | 297 | # GhostDoc plugin setting file 298 | *.GhostDoc.xml 299 | 300 | # Node.js Tools for Visual Studio 301 | .ntvs_analysis.dat 302 | node_modules/ 303 | 304 | # Visual Studio 6 build log 305 | *.plg 306 | 307 | # Visual Studio 6 workspace options file 308 | *.opt 309 | 310 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 311 | *.vbw 312 | 313 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 314 | *.vbp 315 | 316 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 317 | *.dsw 318 | *.dsp 319 | 320 | # Visual Studio 6 technical files 321 | *.ncb 322 | *.aps 323 | 324 | # Visual Studio LightSwitch build output 325 | **/*.HTMLClient/GeneratedArtifacts 326 | **/*.DesktopClient/GeneratedArtifacts 327 | **/*.DesktopClient/ModelManifest.xml 328 | **/*.Server/GeneratedArtifacts 329 | **/*.Server/ModelManifest.xml 330 | _Pvt_Extensions 331 | 332 | # Paket dependency manager 333 | **/.paket/paket.exe 334 | paket-files/ 335 | 336 | # FAKE - F# Make 337 | **/.fake/ 338 | 339 | # CodeRush personal settings 340 | **/.cr/personal 341 | 342 | # Python Tools for Visual Studio (PTVS) 343 | **/__pycache__/ 344 | *.pyc 345 | 346 | # Cake - Uncomment if you are using it 347 | #tools/** 348 | #!tools/packages.config 349 | 350 | # Tabs Studio 351 | *.tss 352 | 353 | # Telerik's JustMock configuration file 354 | *.jmconfig 355 | 356 | # BizTalk build output 357 | *.btp.cs 358 | *.btm.cs 359 | *.odx.cs 360 | *.xsd.cs 361 | 362 | # OpenCover UI analysis results 363 | OpenCover/ 364 | 365 | # Azure Stream Analytics local run output 366 | ASALocalRun/ 367 | 368 | # MSBuild Binary and Structured Log 369 | *.binlog 370 | MSBuild_Logs/ 371 | 372 | # AWS SAM Build and Temporary Artifacts folder 373 | .aws-sam 374 | 375 | # NVidia Nsight GPU debugger configuration file 376 | *.nvuser 377 | 378 | # MFractors (Xamarin productivity tool) working folder 379 | **/.mfractor/ 380 | 381 | # Local History for Visual Studio 382 | **/.localhistory/ 383 | 384 | # Visual Studio History (VSHistory) files 385 | .vshistory/ 386 | 387 | # BeatPulse healthcheck temp database 388 | healthchecksdb 389 | 390 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 391 | MigrationBackup/ 392 | 393 | # Ionide (cross platform F# VS Code tools) working folder 394 | **/.ionide/ 395 | 396 | # Fody - auto-generated XML schema 397 | FodyWeavers.xsd 398 | 399 | # VS Code files for those working on multiple tools 400 | .vscode/* 401 | !.vscode/settings.json 402 | !.vscode/tasks.json 403 | !.vscode/launch.json 404 | !.vscode/extensions.json 405 | !.vscode/*.code-snippets 406 | 407 | # Local History for Visual Studio Code 408 | .history/ 409 | 410 | # Built Visual Studio Code Extensions 411 | *.vsix 412 | 413 | # Windows Installer files from build outputs 414 | *.cab 415 | *.msi 416 | *.msix 417 | *.msm 418 | *.msp 419 | -------------------------------------------------------------------------------- /tests/AspNetCore.ReferrerBlock.Tests/ReferrerBlockMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using ReferrerBlock.Configuration; 2 | using ReferrerBlock.Middleware; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Logging; 5 | using Moq; 6 | using System.Net; 7 | 8 | namespace AspNetCore.ReferrerBlock.Tests; 9 | 10 | [TestClass] 11 | public class ReferrerBlockMiddlewareTests 12 | { 13 | private Mock _nextMock; 14 | private Mock> _loggerMock; 15 | private ReferrerBlockOptions _options; 16 | private DefaultHttpContext _httpContext; 17 | 18 | [TestInitialize] 19 | public void Setup() 20 | { 21 | _nextMock = new Mock(); 22 | _loggerMock = new Mock>(); 23 | _options = new ReferrerBlockOptions(); 24 | _httpContext = new DefaultHttpContext(); 25 | _httpContext.Connection.RemoteIpAddress = IPAddress.Parse("192.168.1.1"); 26 | _httpContext.Request.Path = "/test"; 27 | } 28 | 29 | #region Tests sans Referer 30 | 31 | [TestMethod] 32 | public async Task NoReferer_ShouldCallNext() 33 | { 34 | // Arrange 35 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 36 | 37 | // Act 38 | await middleware.InvokeAsync(_httpContext); 39 | 40 | // Assert 41 | _nextMock.Verify(next => next(_httpContext), Times.Once); 42 | } 43 | 44 | [TestMethod] 45 | public async Task EmptyReferer_ShouldCallNext() 46 | { 47 | // Arrange 48 | _httpContext.Request.Headers.Referer = string.Empty; 49 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 50 | 51 | // Act 52 | await middleware.InvokeAsync(_httpContext); 53 | 54 | // Assert 55 | _nextMock.Verify(next => next(_httpContext), Times.Once); 56 | } 57 | 58 | #endregion 59 | 60 | #region Tests avec Referer valide (non bloqué) 61 | 62 | [TestMethod] 63 | public async Task ValidReferer_NotBlocked_ShouldCallNext() 64 | { 65 | // Arrange 66 | _httpContext.Request.Headers.Referer = "https://www.google.com"; 67 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 68 | 69 | // Act 70 | await middleware.InvokeAsync(_httpContext); 71 | 72 | // Assert 73 | _nextMock.Verify(next => next(_httpContext), Times.Once); 74 | Assert.AreNotEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 75 | } 76 | 77 | [TestMethod] 78 | public async Task ValidReferer_AllowedTLD_ShouldCallNext() 79 | { 80 | // Arrange 81 | _httpContext.Request.Headers.Referer = "https://example.com"; 82 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 83 | 84 | // Act 85 | await middleware.InvokeAsync(_httpContext); 86 | 87 | // Assert 88 | _nextMock.Verify(next => next(_httpContext), Times.Once); 89 | } 90 | 91 | #endregion 92 | 93 | #region Tests TLD bloqués 94 | 95 | [TestMethod] 96 | public async Task BlockedTLD_ICU_ShouldReturn410() 97 | { 98 | // Arrange 99 | _httpContext.Request.Headers.Referer = "https://spam.icu"; 100 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 101 | 102 | // Act 103 | await middleware.InvokeAsync(_httpContext); 104 | 105 | // Assert 106 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 107 | _nextMock.Verify(next => next(_httpContext), Times.Never); 108 | } 109 | 110 | [TestMethod] 111 | public async Task BlockedTLD_XYZ_ShouldReturn410() 112 | { 113 | // Arrange 114 | _httpContext.Request.Headers.Referer = "https://malicious.xyz"; 115 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 116 | 117 | // Act 118 | await middleware.InvokeAsync(_httpContext); 119 | 120 | // Assert 121 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 122 | _nextMock.Verify(next => next(_httpContext), Times.Never); 123 | } 124 | 125 | [TestMethod] 126 | public async Task BlockedTLD_BizId_ShouldReturn410() 127 | { 128 | // Arrange 129 | _httpContext.Request.Headers.Referer = "https://spam.biz.id"; 130 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 131 | 132 | // Act 133 | await middleware.InvokeAsync(_httpContext); 134 | 135 | // Assert 136 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 137 | } 138 | 139 | [TestMethod] 140 | public async Task BlockedTLD_id_ShouldReturn410() 141 | { 142 | // Arrange 143 | _httpContext.Request.Headers.Referer = "hk4.ikancupang.id"; 144 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 145 | 146 | // Act 147 | await middleware.InvokeAsync(_httpContext); 148 | 149 | // Assert 150 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 151 | } 152 | 153 | [TestMethod] 154 | public async Task BlockedTLD_CaseInsensitive_ShouldReturn410() 155 | { 156 | // Arrange 157 | _httpContext.Request.Headers.Referer = "https://SPAM.ICU"; 158 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 159 | 160 | // Act 161 | await middleware.InvokeAsync(_httpContext); 162 | 163 | // Assert 164 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 165 | } 166 | 167 | #endregion 168 | 169 | #region Tests domaines bloqués 170 | 171 | [TestMethod] 172 | public async Task BlockedDomain_Exact_ShouldReturn410() 173 | { 174 | // Arrange 175 | _httpContext.Request.Headers.Referer = "https://gazdp.com"; 176 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 177 | 178 | // Act 179 | await middleware.InvokeAsync(_httpContext); 180 | 181 | // Assert 182 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 183 | _nextMock.Verify(next => next(_httpContext), Times.Never); 184 | } 185 | 186 | [TestMethod] 187 | public async Task BlockedDomain_Subdomain_ShouldReturn410() 188 | { 189 | // Arrange 190 | _httpContext.Request.Headers.Referer = "https://www.gazdp.com"; 191 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 192 | 193 | // Act 194 | await middleware.InvokeAsync(_httpContext); 195 | 196 | // Assert 197 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 198 | } 199 | 200 | [TestMethod] 201 | public async Task BlockedDomain_DeepSubdomain_ShouldReturn410() 202 | { 203 | // Arrange 204 | _httpContext.Request.Headers.Referer = "https://sub.example.gazdp.com"; 205 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 206 | 207 | // Act 208 | await middleware.InvokeAsync(_httpContext); 209 | 210 | // Assert 211 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 212 | } 213 | 214 | [TestMethod] 215 | public async Task BlockedDomain_CaseInsensitive_ShouldReturn410() 216 | { 217 | // Arrange 218 | _httpContext.Request.Headers.Referer = "https://GAZDP.COM"; 219 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 220 | 221 | // Act 222 | await middleware.InvokeAsync(_httpContext); 223 | 224 | // Assert 225 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 226 | } 227 | 228 | [TestMethod] 229 | public async Task BlockedDomain_MojokCo_Subdomain_ShouldReturn410() 230 | { 231 | // Arrange 232 | _httpContext.Request.Headers.Referer = "https://lifestyle.mojok.co/"; 233 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 234 | 235 | // Act 236 | await middleware.InvokeAsync(_httpContext); 237 | 238 | // Assert 239 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 240 | _nextMock.Verify(next => next(_httpContext), Times.Never); 241 | } 242 | 243 | #endregion 244 | 245 | #region Tests subdomain prefixes bloqués 246 | 247 | [TestMethod] 248 | public async Task BlockedSubdomainPrefix_Iqri_ShouldReturn410() 249 | { 250 | // Arrange 251 | _httpContext.Request.Headers.Referer = "https://iqri.example.com"; 252 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 253 | 254 | // Act 255 | await middleware.InvokeAsync(_httpContext); 256 | 257 | // Assert 258 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 259 | _nextMock.Verify(next => next(_httpContext), Times.Never); 260 | } 261 | 262 | [TestMethod] 263 | public async Task BlockedSubdomainPrefix_IqriWithDigit_ShouldReturn410() 264 | { 265 | // Arrange 266 | _httpContext.Request.Headers.Referer = "https://iqri1.spammer.net"; 267 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 268 | 269 | // Act 270 | await middleware.InvokeAsync(_httpContext); 271 | 272 | // Assert 273 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 274 | _nextMock.Verify(next => next(_httpContext), Times.Never); 275 | } 276 | 277 | [TestMethod] 278 | public async Task BlockedSubdomainPrefix_IqriWithMultipleDigits_ShouldReturn410() 279 | { 280 | // Arrange 281 | _httpContext.Request.Headers.Referer = "https://iqri18.malicious.org"; 282 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 283 | 284 | // Act 285 | await middleware.InvokeAsync(_httpContext); 286 | 287 | // Assert 288 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 289 | _nextMock.Verify(next => next(_httpContext), Times.Never); 290 | } 291 | 292 | [TestMethod] 293 | public async Task BlockedSubdomainPrefix_Hk_ShouldReturn410() 294 | { 295 | // Arrange 296 | _httpContext.Request.Headers.Referer = "https://hk.spam.com"; 297 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 298 | 299 | // Act 300 | await middleware.InvokeAsync(_httpContext); 301 | 302 | // Assert 303 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 304 | _nextMock.Verify(next => next(_httpContext), Times.Never); 305 | } 306 | 307 | [TestMethod] 308 | public async Task BlockedSubdomainPrefix_HkWithDigits_ShouldReturn410() 309 | { 310 | // Arrange 311 | _httpContext.Request.Headers.Referer = "https://hk12.badsite.net"; 312 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 313 | 314 | // Act 315 | await middleware.InvokeAsync(_httpContext); 316 | 317 | // Assert 318 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 319 | _nextMock.Verify(next => next(_httpContext), Times.Never); 320 | } 321 | 322 | [TestMethod] 323 | public async Task BlockedSubdomainPrefix_CaseInsensitive_ShouldReturn410() 324 | { 325 | // Arrange 326 | _httpContext.Request.Headers.Referer = "https://IQRI5.EXAMPLE.COM"; 327 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 328 | 329 | // Act 330 | await middleware.InvokeAsync(_httpContext); 331 | 332 | // Assert 333 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 334 | } 335 | 336 | [TestMethod] 337 | public async Task BlockedSubdomainPrefix_WithLettersAfterDigits_ShouldNotBlock() 338 | { 339 | // Arrange - "iqri1x" should NOT match because it has letters after digits 340 | _httpContext.Request.Headers.Referer = "https://iqri1x.example.com"; 341 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 342 | 343 | // Act 344 | await middleware.InvokeAsync(_httpContext); 345 | 346 | // Assert 347 | _nextMock.Verify(next => next(_httpContext), Times.Once); 348 | Assert.AreNotEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 349 | } 350 | 351 | [TestMethod] 352 | public async Task BlockedSubdomainPrefix_NotAtStart_ShouldNotBlock() 353 | { 354 | // Arrange - "subiqri1" should NOT match because iqri is not at the start 355 | _httpContext.Request.Headers.Referer = "https://subiqri1.example.com"; 356 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 357 | 358 | // Act 359 | await middleware.InvokeAsync(_httpContext); 360 | 361 | // Assert 362 | _nextMock.Verify(next => next(_httpContext), Times.Once); 363 | Assert.AreNotEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 364 | } 365 | 366 | [TestMethod] 367 | public async Task BlockedSubdomainPrefix_InDomainName_ShouldNotBlock() 368 | { 369 | // Arrange - "iqri" in domain name (not subdomain) should NOT be blocked by prefix rules 370 | _httpContext.Request.Headers.Referer = "https://iqrisite.com"; 371 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 372 | 373 | // Act 374 | await middleware.InvokeAsync(_httpContext); 375 | 376 | // Assert 377 | _nextMock.Verify(next => next(_httpContext), Times.Once); 378 | Assert.AreNotEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 379 | } 380 | 381 | #endregion 382 | 383 | #region Tests patterns bloqués 384 | 385 | [TestMethod] 386 | public async Task BlockedPattern_ShouldReturn410() 387 | { 388 | // Arrange 389 | _httpContext.Request.Headers.Referer = "https://ctysss.example.com"; 390 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 391 | 392 | // Act 393 | await middleware.InvokeAsync(_httpContext); 394 | 395 | // Assert 396 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 397 | _nextMock.Verify(next => next(_httpContext), Times.Never); 398 | } 399 | 400 | [TestMethod] 401 | public async Task BlockedPattern_InMiddle_ShouldReturn410() 402 | { 403 | // Arrange 404 | _httpContext.Request.Headers.Referer = "https://shop-ctysss-online.com"; 405 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 406 | 407 | // Act 408 | await middleware.InvokeAsync(_httpContext); 409 | 410 | // Assert 411 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 412 | } 413 | 414 | [TestMethod] 415 | public async Task BlockedPattern_CaseInsensitive_ShouldReturn410() 416 | { 417 | // Arrange 418 | _httpContext.Request.Headers.Referer = "https://CTYSSS.example.com"; 419 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 420 | 421 | // Act 422 | await middleware.InvokeAsync(_httpContext); 423 | 424 | // Assert 425 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 426 | } 427 | 428 | #endregion 429 | 430 | #region Tests d'erreurs 431 | 432 | [TestMethod] 433 | public async Task MalformedReferer_ShouldCallNext() 434 | { 435 | // Arrange - Use a truly malformed URI that will throw UriFormatException 436 | _httpContext.Request.Headers.Referer = "https://[invalid-ipv6"; 437 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 438 | 439 | // Act 440 | await middleware.InvokeAsync(_httpContext); 441 | 442 | // Assert 443 | _nextMock.Verify(next => next(_httpContext), Times.Once); 444 | _loggerMock.Verify( 445 | x => x.Log( 446 | LogLevel.Debug, 447 | It.IsAny(), 448 | It.Is((v, t) => v.ToString()!.Contains("Malformed referrer detected")), 449 | It.IsAny(), 450 | It.IsAny>()), 451 | Times.Once); 452 | } 453 | 454 | #endregion 455 | 456 | #region Tests de logging 457 | 458 | [TestMethod] 459 | public async Task BlockedReferer_ShouldLogWarning() 460 | { 461 | // Arrange 462 | _httpContext.Request.Headers.Referer = "https://spam.icu"; 463 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 464 | 465 | // Act 466 | await middleware.InvokeAsync(_httpContext); 467 | 468 | // Assert 469 | _loggerMock.Verify( 470 | x => x.Log( 471 | LogLevel.Warning, 472 | It.IsAny(), 473 | It.Is((v, t) => v.ToString()!.Contains("Referrer spam blocked")), 474 | It.IsAny(), 475 | It.IsAny>()), 476 | Times.Once); 477 | } 478 | 479 | #endregion 480 | 481 | #region Tests de réponse 482 | 483 | [TestMethod] 484 | public async Task BlockedReferer_ShouldReturnHTMLResponse() 485 | { 486 | // Arrange 487 | _httpContext.Request.Headers.Referer = "https://spam.icu"; 488 | _httpContext.Response.Body = new MemoryStream(); 489 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 490 | 491 | // Act 492 | await middleware.InvokeAsync(_httpContext); 493 | 494 | // Assert 495 | _httpContext.Response.Body.Seek(0, SeekOrigin.Begin); 496 | using var reader = new StreamReader(_httpContext.Response.Body); 497 | var responseBody = await reader.ReadToEndAsync(); 498 | 499 | Assert.IsTrue(responseBody.Contains("")); 500 | Assert.IsTrue(responseBody.Contains("Gone")); 501 | } 502 | 503 | [TestMethod] 504 | public async Task BlockedReferer_ShouldIncludeDelay() 505 | { 506 | // Arrange 507 | _httpContext.Request.Headers.Referer = "https://spam.icu"; 508 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 509 | var stopwatch = System.Diagnostics.Stopwatch.StartNew(); 510 | 511 | // Act 512 | await middleware.InvokeAsync(_httpContext); 513 | stopwatch.Stop(); 514 | 515 | // Assert 516 | Assert.IsTrue(stopwatch.ElapsedMilliseconds >= 100, "Le délai devrait être d'au moins 100ms"); 517 | Assert.IsTrue(stopwatch.ElapsedMilliseconds < 600, "Le délai ne devrait pas dépasser 500ms + marge"); 518 | } 519 | 520 | #endregion 521 | 522 | #region Tests de scénarios réels 523 | 524 | [TestMethod] 525 | public async Task MultipleBlockedDomains_ShouldBlockAll() 526 | { 527 | // Arrange 528 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 529 | var blockedUrls = new[] 530 | { 531 | "https://gazdp.com", 532 | "https://spam.icu", 533 | "https://ctysss-shop.com" 534 | }; 535 | 536 | foreach (var url in blockedUrls) 537 | { 538 | var context = new DefaultHttpContext(); 539 | context.Request.Headers.Referer = url; 540 | context.Connection.RemoteIpAddress = IPAddress.Parse("192.168.1.1"); 541 | 542 | // Act 543 | await middleware.InvokeAsync(context); 544 | 545 | // Assert 546 | Assert.AreEqual(StatusCodes.Status410Gone, context.Response.StatusCode, $"Failed for URL: {url}"); 547 | } 548 | } 549 | 550 | [TestMethod] 551 | public async Task HTTPAndHTTPS_BothShouldBeBlocked() 552 | { 553 | // Arrange 554 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 555 | 556 | var httpContext = new DefaultHttpContext(); 557 | httpContext.Request.Headers.Referer = "http://spam.icu"; 558 | httpContext.Connection.RemoteIpAddress = IPAddress.Parse("192.168.1.1"); 559 | 560 | var httpsContext = new DefaultHttpContext(); 561 | httpsContext.Request.Headers.Referer = "https://spam.icu"; 562 | httpsContext.Connection.RemoteIpAddress = IPAddress.Parse("192.168.1.1"); 563 | 564 | // Act 565 | await middleware.InvokeAsync(httpContext); 566 | await middleware.InvokeAsync(httpsContext); 567 | 568 | // Assert 569 | Assert.AreEqual(StatusCodes.Status410Gone, httpContext.Response.StatusCode); 570 | Assert.AreEqual(StatusCodes.Status410Gone, httpsContext.Response.StatusCode); 571 | } 572 | 573 | #endregion 574 | 575 | #region Tests des cas limites (Edge Cases) 576 | 577 | [TestMethod] 578 | public async Task InvokeAsync_BlockedTLD_WithPort_ShouldReturn410() 579 | { 580 | // Arrange 581 | _httpContext.Request.Headers.Referer = "https://spam.icu:8080/path"; 582 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 583 | 584 | // Act 585 | await middleware.InvokeAsync(_httpContext); 586 | 587 | // Assert 588 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 589 | _nextMock.Verify(next => next(_httpContext), Times.Never); 590 | } 591 | 592 | [TestMethod] 593 | public async Task InvokeAsync_BlockedTLD_WithQueryString_ShouldReturn410() 594 | { 595 | // Arrange 596 | _httpContext.Request.Headers.Referer = "https://spam.icu?param=value&other=test"; 597 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 598 | 599 | // Act 600 | await middleware.InvokeAsync(_httpContext); 601 | 602 | // Assert 603 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 604 | } 605 | 606 | [TestMethod] 607 | public async Task InvokeAsync_BlockedTLD_WithFragment_ShouldReturn410() 608 | { 609 | // Arrange 610 | _httpContext.Request.Headers.Referer = "https://spam.icu#section"; 611 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 612 | 613 | // Act 614 | await middleware.InvokeAsync(_httpContext); 615 | 616 | // Assert 617 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 618 | } 619 | 620 | [TestMethod] 621 | public async Task InvokeAsync_BlockedTLD_WithPath_ShouldReturn410() 622 | { 623 | // Arrange 624 | _httpContext.Request.Headers.Referer = "https://spam.icu/deep/path/to/page.html"; 625 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 626 | 627 | // Act 628 | await middleware.InvokeAsync(_httpContext); 629 | 630 | // Assert 631 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 632 | } 633 | 634 | [TestMethod] 635 | public async Task InvokeAsync_MultipleSubdomains_BlockedTLD_ShouldReturn410() 636 | { 637 | // Arrange 638 | _httpContext.Request.Headers.Referer = "https://deep.sub.domain.spam.icu"; 639 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 640 | 641 | // Act 642 | await middleware.InvokeAsync(_httpContext); 643 | 644 | // Assert 645 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 646 | } 647 | 648 | [TestMethod] 649 | public async Task InvokeAsync_IPv4Address_ShouldCallNext() 650 | { 651 | // Arrange 652 | _httpContext.Request.Headers.Referer = "http://192.168.1.1"; 653 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 654 | 655 | // Act 656 | await middleware.InvokeAsync(_httpContext); 657 | 658 | // Assert 659 | _nextMock.Verify(next => next(_httpContext), Times.Once); 660 | Assert.AreNotEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 661 | } 662 | 663 | [TestMethod] 664 | public async Task InvokeAsync_ComplexURL_AllComponents_BlockedTLD_ShouldReturn410() 665 | { 666 | // Arrange 667 | _httpContext.Request.Headers.Referer = "https://sub.spam.icu:8080/path/to/page?param1=value1¶m2=value2#section"; 668 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 669 | 670 | // Act 671 | await middleware.InvokeAsync(_httpContext); 672 | 673 | // Assert 674 | Assert.AreEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 675 | } 676 | 677 | [TestMethod] 678 | public async Task InvokeAsync_LegitimateSubdomain_SimilarToBlocked_ShouldNotBlock() 679 | { 680 | // Arrange - Verify that "notgazdp.com" is NOT blocked when "gazdp.com" is blocked 681 | _httpContext.Request.Headers.Referer = "https://notgazdp.com"; 682 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, _options); 683 | 684 | // Act 685 | await middleware.InvokeAsync(_httpContext); 686 | 687 | // Assert 688 | _nextMock.Verify(next => next(_httpContext), Times.Once); 689 | Assert.AreNotEqual(StatusCodes.Status410Gone, _httpContext.Response.StatusCode); 690 | } 691 | 692 | [TestMethod] 693 | public async Task InvokeAsync_EmptyStringInOptions_ShouldNotCrash() 694 | { 695 | // Arrange 696 | var customOptions = new ReferrerBlockOptions(); 697 | customOptions.BlockedDomains.Add(string.Empty); 698 | customOptions.BlockedPatterns.Add(string.Empty); 699 | _httpContext.Request.Headers.Referer = "https://example.com"; 700 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, customOptions); 701 | 702 | // Act 703 | await middleware.InvokeAsync(_httpContext); 704 | 705 | // Assert - Should not crash, should process normally 706 | _nextMock.Verify(next => next(_httpContext), Times.Once); 707 | } 708 | 709 | [TestMethod] 710 | public async Task InvokeAsync_NullCollectionsInOptions_ShouldNotCrash() 711 | { 712 | // Arrange 713 | var customOptions = new ReferrerBlockOptions 714 | { 715 | BlockedDomains = null!, 716 | BlockedTLDs = null!, 717 | BlockedPatterns = null!, 718 | BlockedSubdomainPrefixes = null! 719 | }; 720 | _httpContext.Request.Headers.Referer = "https://spam.icu"; 721 | var middleware = new ReferrerBlockMiddleware(_nextMock.Object, _loggerMock.Object, customOptions); 722 | 723 | // Act 724 | await middleware.InvokeAsync(_httpContext); 725 | 726 | // Assert - Should not crash, should allow through (no blocking rules) 727 | _nextMock.Verify(next => next(_httpContext), Times.Once); 728 | } 729 | 730 | #endregion 731 | } --------------------------------------------------------------------------------