Skip to content

Commit

Permalink
v1.1
Browse files Browse the repository at this point in the history
  • Loading branch information
kimboslice99 committed Jan 21, 2025
1 parent f5c6003 commit 3016f44
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 74 deletions.
151 changes: 86 additions & 65 deletions CloudflareIpModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,26 @@
using System.Configuration;
using NetTools;
using System.Net;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace CloudflareProxyTrust
{
public class CloudflareIpModule : IHttpModule
{
private static string[] _cfips;
private static Timer _timer;
private List<IPAddressRange> _cfips = new List<IPAddressRange>();
private DateTime _lastupdated = DateTime.MinValue;
private int count = 0;

#region IHttpModule implementation

public void Dispose()
{
if (_timer != null)
{
_timer.Dispose();
_timer = null;
}

if (_cfips != null)
{
_cfips = null;
}
}
public void Dispose() { }

public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(Begin);

LoadCfipsData();

// Start a timer to reload cfips data periodically
int reloadInterval = GetReloadInterval(); // Get the reload interval from configuration
if(_timer == null)
_timer = new Timer(ReloadCfipsData, null, TimeSpan.Zero, TimeSpan.FromHours(reloadInterval));
_lastupdated = DateTime.Now;
WriteDbg($"Init {_lastupdated}");
}

public void Begin(Object source, EventArgs e)
Expand All @@ -48,76 +32,113 @@ public void Begin(Object source, EventArgs e)
{
HttpApplication app = (HttpApplication)source;
HttpRequest request = app.Context.Request;

if(_cfips == null)
LoadCfipsData();

// we may not be proxied, or the path may not be configured for this site
if (_cfips == null ||
string.IsNullOrEmpty(request.ServerVariables["HTTP_CF_CONNECTING_IP"]))
// stop the loop caused by defaultdocumentmodule
if (!string.IsNullOrEmpty(request.ServerVariables["CF_SECURE"]))
{
WriteDbg("we handled this already");
return;
}

// maybe we arent proxied
var cfConnectingIp = request.ServerVariables["HTTP_CF_CONNECTING_IP"];
if(AllowNonProxyRemote)
if (string.IsNullOrEmpty(cfConnectingIp))
return;

// or maybe we just havent been configured for this site yet
if (_cfips.Count == 0)
return;

// perhaps iis has been reusing this instance for some time
// since we have no idea how many times or for how long IIS may reuse an instance, we will keep track of when we last loaded the list
if (_lastupdated.AddMinutes(20) < DateTime.Now)
{
WriteDbg($"refreshing iplist {request.UserHostAddress}");
LoadCfipsData();
}
#if DEBUG
else
// some verification for our debug logging that we are indeed reusing instances across requests
count++;
#endif
IPAddress remoteaddr = IPAddress.Parse(request.ServerVariables["REMOTE_ADDR"]);
string cf_connecting_ip = request.ServerVariables["HTTP_CF_CONNECTING_IP"];
bool trusted = false;

foreach (string cfip in _cfips)
foreach (IPAddressRange cfip in _cfips)
{
// Dont try to parse empty lines
if (string.IsNullOrWhiteSpace(cfip))
continue;

IPAddressRange IPRange = IPAddressRange.Parse(cfip);
if (IPRange.Contains(remoteaddr))
if (cfip.Contains(remoteaddr))
{
trusted = true;
// only if trusted we will forward the real IP in REMOTE_ADDR
request.ServerVariables.Set("REMOTE_ADDR", cf_connecting_ip);
// only if trusted, forward the real IP in REMOTE_ADDR
request.ServerVariables.Set("REMOTE_ADDR", cfConnectingIp);

// maybe you want the cloudflare IP? keep it here
request.ServerVariables.Set("HTTP_X_ORIGINAL_ADDR", remoteaddr.ToString());

// we cant rely on checking if X-Original-Addr exists since a malicious client could send this
// it is also useful to stop the defaultdocumentmodule looping
request.ServerVariables.Set("CF_SECURE", "true");
}
}
#if DEBUG
Debug.WriteLine($"[CloudflareProxyTrust]: trusted: {trusted.ToString()} remoteaddr: {remoteaddr} cfip: {cf_connecting_ip} url: {request.RawUrl} host: {request.ServerVariables["HTTP_HOST"]}");
#endif
if(!trusted)
{
// spoofing attempt
request.ServerVariables.Set("CF_SECURE", "false");
// this ends up logged twice, unsure how to prevent this one
EventLog.WriteEntry(".NET Runtime",
$"[CloudflareProxyTrust]: possible spoofing attempt\r\n" +
$"ip:[{request.ServerVariables["REMOTE_ADDR"]}]\r\n" +
$"host:[{request.ServerVariables["SERVER_NAME"]}]\r\n" +
$"url:[{request.RawUrl}]\r\n" +
$"useragent:[{request.UserAgent}]",
EventLogEntryType.Information,
1000
);
}
if (!trusted && DenyUntrusted)
{
app.Response.StatusCode = DenyCode;
app.Response.StatusDescription = DenyDescription;
app.Response.Flush();
app.CompleteRequest();
}

WriteDbg($"trusted: {trusted} remoteaddr: {remoteaddr} cfip: {cfConnectingIp} url: {request.RawUrl} host: {request.ServerVariables["HTTP_HOST"]} reused {count} times");
}
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine("[CloudflareProxyTrust]: " + ex.Message);
#endif
WriteDbg("Exception occured " + ex.Message);
}
}

private static void LoadCfipsData()
{
var path = ConfigurationManager.AppSettings["CF_IP_Path"];
// this one to deny if untrusted
private static bool DenyUntrusted => Convert.ToBoolean(ConfigurationManager.AppSettings["CF_DenyUntrusted"]);
// this one to allow direct connections to the server, only has effect if were denying untrusted.
private static bool AllowNonProxyRemote => Convert.ToBoolean(ConfigurationManager.AppSettings["CF_AllowNonProxyRemote"] ?? "true");

if (!string.IsNullOrEmpty(path))
{
// Read lines into string array and store in cfips
_cfips = File.ReadAllLines(path);
}
}
private static int DenyCode => Convert.ToInt16(ConfigurationManager.AppSettings["CF_DenyCode"] ?? "400");

private static string DenyDescription => ConfigurationManager.AppSettings["CF_DenyDescription"] ?? "Bad Request";

private static int GetReloadInterval()
private void LoadCfipsData()
{
// Get the reload interval from configuration, default to 24 hours
int interval;
if (!int.TryParse(ConfigurationManager.AppSettings["CF_IP_ReloadInterval"], out interval))
var path = ConfigurationManager.AppSettings["CF_IP_Path"];
if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
interval = 24; // Default reload interval (in hours)
string[] filelines = File.ReadAllText(path).Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in filelines)
{
if (IPAddressRange.TryParse(line, out IPAddressRange ipRange))
_cfips.Add(ipRange);
}
}
return interval;
}

private static void ReloadCfipsData(object state)
private static void WriteDbg(string msg)
{
// Reload cfips data periodically
LoadCfipsData();
#if DEBUG
Debug.WriteLine($"[CloudflareProxyTrust]: {msg}");
#endif
}
#endregion
}
}
1 change: 1 addition & 0 deletions CloudflareProxyTrust.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<None Include="IPAddressRange\IPAddressRange.ruleset" />
<None Include="IPAddressRange\IPAddressRange.Signed.nuspec" />
<None Include="IPAddressRange\key.snk" />
<None Include="README.md" />
</ItemGroup>
<ItemGroup>
<Content Include="IPAddressRange\.gitignore" />
Expand Down
6 changes: 3 additions & 3 deletions Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CloudflareProxyTrust")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyCopyright("Copyright © kimboslice99 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyKeyFile("CloudflareProxyTrust.snk")]
Expand All @@ -33,5 +33,5 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.1.0.0")]
[assembly: AssemblyFileVersion("1.1.0.0")]
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ This module addresses security concerns related to Cloudflare by ensuring that t

## Purpose

Utilizing URL rewrite to replace `REMOTE_ADDR` with `HTTP_CF_CONNECTING_IP` might seem like a solution. However, it's important to note that `Cf-Connecting-Ip` is a header that can be spoofed by anyone. Therefore, it should only be relied upon when the source is verifiable.
Utilizing URL rewrite to replace `REMOTE_ADDR` with `HTTP_CF_CONNECTING_IP` might seem like a solution. However, it's important to note that `CF-Connecting-IP` is a header that can be spoofed by anyone. Therefore, it should only be relied upon when the source is verifiable.

This module serves to replace the `REMOTE_ADDR` variable with `HTTP_CF_CONNECTING_IP` exclusively if the request originates from a trusted IP address.
This module serves to replace the `REMOTE_ADDR` variable with `HTTP_CF_CONNECTING_IP` exclusively if the request originates from a trusted IP address.

Optionally you may configure this module to allow only connections through CF proxy, or to just block any spoofing attempts.

## Setup

Expand All @@ -17,6 +19,7 @@ This module serves to replace the `REMOTE_ADDR` variable with `HTTP_CF_CONNECTIN
Run the following script, perhaps scheduled to execute once a day, to download Cloudflare IPs and save them into a file:

```powershell
# note that the use of -UseBasicParsing allows limited user accounts to run irm/iwr!
$v4 = irm https://www.cloudflare.com/ips-v4 -UseBasicParsing
$v6 = irm https://www.cloudflare.com/ips-v6 -UseBasicParsing
"$v4`n$v6" | Out-File -NoNewLine CF_IPs.txt
Expand All @@ -25,7 +28,13 @@ Run the following script, perhaps scheduled to execute once a day, to download C
## Deployment

- For deployment to a single site, copy `CloudflareProxyTrust.dll` into your bin folder.
- For deployment to the entire webserver, add the DLL to the Global Assembly Cache (GAC) and `%SystemDrive%\Windows\System32\inetsrv`.
- For deployment to the entire webserver, add the DLL to the Global Assembly Cache (GAC). You can do so with gacutil or the following (admin) powershell commands.
```powershell
$dllpath = "C:\full\path\to\CloudflareProxyTrust.dll"
[System.Reflection.Assembly]::Load("System.EnterpriseServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") | Out-Null
$publish = New-Object System.EnterpriseServices.Internal.Publish
$publish.GacInstall($dllpath)
```

**Configure IIS**

Expand All @@ -34,11 +43,33 @@ Run the following script, perhaps scheduled to execute once a day, to download C
```xml
<system.webServer>
<modules>
<add name="CloudflareProxyTrust" type="CloudflareProxyTrust.CloudflareIpModule, CloudflareProxyTrust, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7381665d8f939351" preCondition="runtimeVersionv4.0" />
<add name="CloudflareProxyTrust" type="CloudflareProxyTrust.CloudflareIpModule, CloudflareProxyTrust, Version=1.1.0.0, Culture=neutral, PublicKeyToken=7381665d8f939351" preCondition="runtimeVersionv4.0" />
</modules>
</system.webServer>
```

**Application Setting**
**Settings**

Create an Application Setting key named `CF_IP_Path` pointing to the file containing Cloudflare's proxy IPs, and ensure IIS_IUSRS has read access to this file. This is the only required setting for the module to function.

All of the following configuation choices set REMOTE_ADDR, but only some of them block.
Note that for CF_AllowNonProxyRemote=false to function CF_DenyUntrusted must be true

Scenario 1. (blocking mode)

You have a site that is proxied, and it should be the only route a legitimate client takes
Set CF_DenyUntrusted to true and CF_AllowNonProxyRemote to false. This will block all clients that arent coming through the CF proxy.

Scenario 2. (blocking mode)

You have a site that is proxied but also has an unproxied name, so clients can utilize CF or not if chosen.
You wish to block any request that comes with CF headers that is not a CF proxy IP (spoofing attempt).
Set CF_DenyUntrusted to true and CF_AllowNonProxyRemote to true

Scenario 3. (default/silent/non-blocking mode)

You have a site that is proxied and may have unproxied names.
You would like to handle REMOTE_ADDR replacement silently, if it exists and is trusted.
Set CF_DenyUntrusted to false and CF_AllowNonProxyRemote to true (these are the defaults)

Create an Application Setting key named `CF_IP_Path` pointing to the file containing Cloudflare's proxy IPs.
Application setting key CF_DenyCode and CF_DenyDescription will allow you to customize the deny response from the default 400 Bad Request.

0 comments on commit 3016f44

Please sign in to comment.