Skip to content

Commit 21e3c81

Browse files
ervwalterclaude
andcommitted
🔒️ fix: improve proxy header security and clean up debugging code
- Add host header validation middleware to prevent header injection attacks - Configure ForwardedHeaders middleware for Cloudflare + DigitalOcean proxy chain - Remove debugging logs from Apple callback controller - Disable hot reload for backend API to ensure config changes apply - Add AllowedHosts configuration to .env.example - Update Vite allowed hosts configuration Security improvements: - Validate incoming host headers against allowed list in production - Trust up to 2 proxies in chain (Cloudflare -> DigitalOcean) - Only process requests to configured domains 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent dc50451 commit 21e3c81

5 files changed

Lines changed: 53 additions & 32 deletions

File tree

.env.example

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@ Jwt__SigningKey=your-random-signing-key-at-least-32-chars
1414

1515
# OAuth Provider Configuration
1616
Withings__ClientId=your-withings-client-id
17-
Withings__ClientSecret=your-withings-client-secret
17+
Withings__ClientSecret=your-withings-client-secret
18+
19+
# Security Configuration
20+
# Comma-separated list of allowed host headers (for production)
21+
# Use "*" to allow all hosts (not recommended for production)
22+
AllowedHosts=trendweight.com,www.trendweight.com,trendweight.io

.tmuxinator.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ windows:
2121
- cd apps/api/TrendWeight
2222
- tmux pipe-pane -t 1 "perl -pe '$|=1; s/\\e\\[[0-9;?]*[a-zA-Z]//g; s/\\\\\\].*?\\\\//g; s/\\e\\].*?\\007//g; s/\\r//g; s/\\e=//g' > logs/backend.log 2>&1"
2323
- clear
24-
- exec bash -c "trap 'tmux kill-session -t trendweight' EXIT; dotnet watch"
24+
- exec bash -c "trap 'tmux kill-session -t trendweight' EXIT; dotnet watch --no-hot-reload"

apps/api/TrendWeight/Features/Auth/AppleCallbackController.cs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,8 @@ namespace TrendWeight.Features.Auth;
99
public class AppleCallbackController : ControllerBase
1010
{
1111
[HttpPost("callback")]
12-
public IActionResult Callback(ILogger<AppleCallbackController> logger)
12+
public IActionResult Callback()
1313
{
14-
// Log the incoming request details (ForwardedHeaders middleware has already updated these)
15-
logger.LogInformation("Apple callback received - Method: {Method}, Scheme: {Scheme}, Host: {Host}, Path: {Path}",
16-
Request.Method, Request.Scheme, Request.Host, Request.Path);
17-
18-
// Log all form data received
19-
foreach (var kvp in Request.Form)
20-
{
21-
// Don't log sensitive data in production, but for debugging this is helpful
22-
var value = kvp.Key.Contains("token", StringComparison.OrdinalIgnoreCase)
23-
? $"[REDACTED-{kvp.Value.ToString().Length}-chars]"
24-
: kvp.Value.ToString();
25-
logger.LogInformation("Apple form data - {Key}: {Value}", kvp.Key, value);
26-
}
27-
2814
// Convert form data to query string
2915
var queryString = string.Join("&", Request.Form.Select(kvp =>
3016
$"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value.ToString())}"));
@@ -33,8 +19,6 @@ public IActionResult Callback(ILogger<AppleCallbackController> logger)
3319
// The ForwardedHeaders middleware has already updated Request.Scheme and Request.Host
3420
var redirectUrl = $"{Request.Scheme}://{Request.Host}/auth/apple/callback?{queryString}";
3521

36-
logger.LogInformation("Redirecting to: {RedirectUrl}", redirectUrl);
37-
3822
return Redirect(redirectUrl);
3923
}
4024
}

apps/api/TrendWeight/Program.cs

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
using System.Net;
22
using System.Text.Json;
33
using System.Text.Json.Serialization;
4+
using Microsoft.AspNetCore.HostFiltering;
45
using Microsoft.AspNetCore.HttpOverrides;
56
using TrendWeight.Infrastructure.Extensions;
67
using TrendWeight.Infrastructure.Middleware;
78

89
var builder = WebApplication.CreateBuilder(args);
910

11+
// Disable ASP.NET Core's built-in host filtering since we have custom validation
12+
builder.Services.Configure<HostFilteringOptions>(options =>
13+
{
14+
options.AllowedHosts = new List<string> { "*" };
15+
});
16+
1017
// Add services to the container
1118
builder.Services.AddControllers()
1219
.AddJsonOptions(options =>
@@ -55,18 +62,14 @@
5562
{
5663
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
5764

58-
if (builder.Environment.IsProduction())
59-
{
60-
// Production: Only trust the immediate proxy (most secure default)
61-
options.ForwardLimit = 1;
62-
options.RequireHeaderSymmetry = false;
63-
}
64-
else
65-
{
66-
// Development: Trust any source for ease of testing
67-
options.KnownNetworks.Clear();
68-
options.KnownProxies.Clear();
69-
}
65+
// Clear default networks/proxies to trust headers from load balancers
66+
// Security Note: Host header validation is performed later in the pipeline
67+
options.KnownNetworks.Clear();
68+
options.KnownProxies.Clear();
69+
70+
// Limit proxy chain depth to prevent spoofing
71+
options.ForwardLimit = 2; // Allows for Cloudflare -> DigitalOcean chain
72+
options.RequireHeaderSymmetry = false;
7073
});
7174

7275
var app = builder.Build();
@@ -80,6 +83,35 @@
8083
// Use forwarded headers from proxies
8184
app.UseForwardedHeaders();
8285

86+
// Validate host header for security (prevent host header injection)
87+
if (app.Environment.IsProduction())
88+
{
89+
app.Use(async (context, next) =>
90+
{
91+
var allowedHostsConfig = app.Configuration["AllowedHosts"];
92+
93+
if (!string.IsNullOrEmpty(allowedHostsConfig) && allowedHostsConfig != "*")
94+
{
95+
var allowedHosts = allowedHostsConfig.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
96+
var requestHost = context.Request.Host.Host;
97+
98+
// Check both with and without port
99+
var hostMatches = allowedHosts.Any(h =>
100+
h.Equals(requestHost, StringComparison.OrdinalIgnoreCase) ||
101+
h.Equals(context.Request.Host.Value, StringComparison.OrdinalIgnoreCase));
102+
103+
if (!hostMatches)
104+
{
105+
app.Logger.LogWarning("Rejected request with invalid host header: {Host}", context.Request.Host.Value);
106+
context.Response.StatusCode = 400;
107+
await context.Response.WriteAsync("Bad Request: Invalid Host header");
108+
return;
109+
}
110+
}
111+
await next();
112+
});
113+
}
114+
83115
if (app.Environment.IsDevelopment())
84116
{
85117
app.UseSwagger();

apps/web/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default defineConfig({
88
plugins: [TanStackRouterVite(), react(), tailwindcss()],
99
server: {
1010
host: true,
11-
allowedHosts: ["studio-1", "studio-1.elf-hadar.ts.net", ".local", "localhost"],
11+
allowedHosts: ["studio-1", "studio-1.elf-hadar.ts.net", ".local", "localhost", ""],
1212
proxy: {
1313
// Proxy all /api requests to the C# backend
1414
"/api": {

0 commit comments

Comments
 (0)