Security Misconfiguration trong ASP.NET Core: debug endpoint, CORS lỏng, và headers thiếu
Security Misconfiguration nhảy từ #5 lên #2 trong OWASP Top 10 2025 — không phải vì lỗi này mới xuất hiện, mà vì attack surface ngày càng rộng hơn: cloud config, container defaults, framework middleware. Với ASP.NET Core, 6 misconfiguration hay gặp nhất là: Swagger/debug endpoints còn bật trong production, CORS quá lỏng, security headers thiếu, error details bị lộ, default credentials không đổi, và server info bị expose. Bài này fix từng cái với code cụ thể. ---
Vấn đề
Trong một pentest gần đây, bước đầu tiên tôi làm là thử các URL quen thuộc:
https://api.target.com/swagger
https://api.target.com/swagger/index.html
https://api.target.com/swagger/v1/swagger.json
URL thứ ba trả về toàn bộ OpenAPI specification — bao gồm mọi endpoint, schema, authentication method, và example request/response. Không cần đăng nhập.
Từ đó, tôi biết chính xác API surface cần test mà không cần dò tìm. Đây là ví dụ điển hình của Security Misconfiguration: không phải lỗi code, mà là config đúng ở dev, sai ở production.
Misconfiguration 1: Swagger và debug endpoints trên production
Vấn đề
// ❌ NGUY HIỂM: Swagger bật unconditionally
var app = builder.Build();
app.UseSwagger(); // Expose OpenAPI spec
app.UseSwaggerUI(); // Expose interactive UI
app.UseDeveloperExceptionPage(); // Expose stack trace đầy đủ
app.UseRouting();
app.MapControllers();
app.Run();
Swagger không chỉ tiện cho developer — nó là bản đồ chi tiết của toàn bộ API cho attacker.
Fix
var app = builder.Build();
// Swagger chỉ trong development environment
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
// Staging: thêm basic auth cho Swagger nếu cần access ngoài dev
});
app.UseDeveloperExceptionPage();
}
else
{
// Production: generic error page, không có stack trace
app.UseExceptionHandler("/error");
app.UseHsts();
}
app.UseRouting();
app.MapControllers();
app.Run();
Nếu cần Swagger trên staging cho QA team, bảo vệ bằng authentication:
// Swagger trên staging — require internal token
if (!app.Environment.IsProduction())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// Hoặc dùng IP whitelist middleware trước Swagger
Misconfiguration 2: CORS AllowAnyOrigin với credentials
Vấn đề
// ❌ NGUY HIỂM: AllowAnyOrigin + AllowCredentials = lỗi phổ biến nhất
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
policy.AllowAnyOrigin() // Bất kỳ site nào cũng request được
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()); // Và gửi kèm cookie/auth header
});
Thực ra AllowAnyOrigin() kết hợp với AllowCredentials() sẽ throw runtime exception trong ASP.NET Core vì browser spec không cho phép. Nhưng nhiều team "fix" bằng cách bỏ AllowCredentials() — khiến CORS mở hoàn toàn cho API không cần credentials, nhưng lại expose data cho cross-site requests.
Fix
// ✅ ĐÚNG: whitelist explicit, khác nhau giữa environments
var allowedOrigins = app.Environment.IsProduction()
? new[] { "https://app.company.com", "https://admin.company.com" }
: new[] { "http://localhost:3000", "http://localhost:5173", "https://staging.company.com" };
builder.Services.AddCors(options =>
{
options.AddPolicy("AppPolicy", policy =>
policy.WithOrigins(allowedOrigins)
.WithMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.WithHeaders("Authorization", "Content-Type", "X-Request-ID")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10)));
});
// Áp dụng policy — không dùng [EnableCors] trên từng controller
app.UseCors("AppPolicy"); // Global, trước UseRouting
Một điểm hay bị bỏ qua: CORS không phải security mechanism server-side — nó là browser enforcement. API của bạn vẫn nhận request từ curl, Postman, hoặc bất kỳ non-browser client nào dù CORS config thế nào. CORS chỉ bảo vệ browser users khỏi cross-site request bất ngờ.
Misconfiguration 3: Security headers thiếu
Vấn đề
Fresh ASP.NET Core project không có security headers theo mặc định. Kiểm tra nhanh bằng curl:
curl -I https://your-api.com/health
# Output thường thấy — nhiều header bị thiếu, server version bị lộ:
HTTP/2 200
Content-Type: application/json
Server: Microsoft-IIS/10.0 ← Lộ server version
X-Powered-By: ASP.NET ← Lộ framework
# Thiếu: X-Content-Type-Options, X-Frame-Options, CSP, HSTS
Fix
// SecurityHeadersMiddleware.cs
public class SecurityHeadersMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
var headers = context.Response.Headers;
// Ngăn MIME sniffing — browser không đoán Content-Type
headers["X-Content-Type-Options"] = "nosniff";
// Ngăn iframe embedding — chống clickjacking
headers["X-Frame-Options"] = "DENY";
// Referrer policy — không leak URL trong cross-origin request
headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
// Content Security Policy — cho API (không phải web app)
// API thường không serve HTML nên CSP đơn giản hơn
headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none'";
// Permissions Policy — tắt browser features không cần thiết
headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()";
// Xóa headers tiết lộ server info
headers.Remove("Server");
headers.Remove("X-Powered-By");
headers.Remove("X-AspNet-Version");
headers.Remove("X-AspNetMvc-Version");
await next(context);
}
}
// Program.cs — đặt trước middleware khác
app.UseMiddleware<SecurityHeadersMiddleware>();
app.UseHsts(); // HSTS header: Strict-Transport-Security
Verify kết quả sau khi add middleware:
curl -I https://your-api.com/health
# Mong muốn:
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY
# Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
# Strict-Transport-Security: max-age=31536000; includeSubDomains
# Không có: Server, X-Powered-By
Misconfiguration 4: Error details lộ ra production
Vấn đề
// ❌ Error handler trả về exception details
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new
{
error = exception?.Message, // "Connection timeout: Server=prod-db-01..."
stack = exception?.StackTrace, // Lộ file paths, line numbers, framework version
type = exception?.GetType().FullName
});
});
});
Stack trace tiết lộ: internal file paths, class names, framework versions — tất cả là thông tin hữu ích cho attacker trong bước reconnaissance.
Fix
// ✅ Log chi tiết internally, response generic
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
// Log đầy đủ để investigate — chỉ internal
logger.LogError(exception,
"Unhandled exception on {Method} {Path} for user {UserId}",
context.Request.Method,
context.Request.Path,
context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "anonymous");
// Response: generic, không có internal details
context.Response.StatusCode = 500;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new
{
type = "https://tools.ietf.org/html/rfc9110#section-15.6.1",
title = "An unexpected error occurred",
status = 500,
// traceId để correlate với internal logs — nhưng không có details
traceId = Activity.Current?.Id ?? context.TraceIdentifier
});
});
});
Misconfiguration 5: Default credentials không đổi
Vấn đề phổ biến trong môi trường containerized
# docker-compose.yml — credentials mặc định từ docs
services:
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
SA_PASSWORD: "YourStrong@Passw0rd" # Copy từ Microsoft docs, không đổi
ACCEPT_EULA: "Y"
redis:
image: redis:7
# Không có auth — mặc định Redis không require password
# Bất kỳ ai access được network đều đọc/ghi được cache
rabbitmq:
image: rabbitmq:3-management
environment:
RABBITMQ_DEFAULT_USER: guest # Default user — nổi tiếng với attackers
RABBITMQ_DEFAULT_PASS: guest # Default pass
Fix
# docker-compose.prod.yml — credentials từ secret manager
services:
db:
environment:
SA_PASSWORD: "${DB_SA_PASSWORD}" # Inject từ environment, không hardcode
redis:
command: redis-server --requirepass "${REDIS_PASSWORD}" --bind 127.0.0.1
rabbitmq:
environment:
RABBITMQ_DEFAULT_USER: "${RABBITMQ_USER}"
RABBITMQ_DEFAULT_PASS: "${RABBITMQ_PASS}"
Và thêm network isolation — services không cần expose port ra host:
services:
api:
ports:
- "443:443" # Chỉ API expose ra ngoài
db:
# Không expose port 1433 — chỉ accessible trong internal network
networks:
- internal
redis:
networks:
- internal
networks:
internal:
driver: bridge
internal: true # Không route ra internet
Misconfiguration 6: Logging quá nhiều hoặc quá ít
Quá nhiều — lộ sensitive data
// ❌ Log toàn bộ request — bao gồm Authorization header, body với credentials
app.UseHttpLogging(); // Mặc định log headers và body
// Log đâu đó trong code
logger.LogInformation("Processing request: {Body}", JsonSerializer.Serialize(request));
// Request body có thể chứa: password, token, credit card
Quá ít — không detect khi bị tấn công
// ❌ Không log auth failures — attacker brute-force không để lại trace
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await _userService.AuthenticateAsync(request.Email, request.Password);
if (user == null)
return Unauthorized(); // Silent failure — không log gì cả
// ...
}
Fix — log đúng thứ, không log sensitive data
// HttpLogging: chỉ log metadata, không log body/headers nhạy cảm
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields =
HttpLoggingFields.RequestMethod |
HttpLoggingFields.RequestPath |
HttpLoggingFields.ResponseStatusCode |
HttpLoggingFields.Duration;
// Không include: RequestBody, ResponseBody, RequestHeaders (có thể chứa Authorization)
});
// Auth events: log đủ để detect + investigate, không log credentials
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await _userService.AuthenticateAsync(request.Email, request.Password);
if (user == null)
{
_logger.LogWarning(
"Failed login attempt for {Email} from {IP} at {Time}",
request.Email, // Email OK — biz context cần để investigate
GetClientIp(HttpContext),
DateTime.UtcNow);
return Unauthorized();
}
_logger.LogInformation("Successful login for user {UserId}", user.Id);
// ...
}
Automated check — thêm vào CI/CD pipeline
#!/bin/bash
# security-headers-check.sh — chạy sau deployment
API_URL="${1:-https://api.staging.company.com}"
FAILED=0
check_header() {
local header=$1
local expected=$2
local value=$(curl -sI "$API_URL/health" | grep -i "^${header}:" | cut -d' ' -f2- | tr -d '\r')
if [ -z "$value" ]; then
echo "❌ MISSING: $header"
FAILED=1
elif [[ "$value" == *"$expected"* ]]; then
echo "✅ OK: $header: $value"
else
echo "⚠️ WRONG: $header = '$value' (expected contains '$expected')"
FAILED=1
fi
}
# Check Swagger không accessible trên production
SWAGGER_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$API_URL/swagger")
if [ "$SWAGGER_STATUS" != "404" ] && [ "$SWAGGER_STATUS" != "401" ]; then
echo "❌ CRITICAL: Swagger accessible in production (HTTP $SWAGGER_STATUS)"
FAILED=1
else
echo "✅ OK: Swagger not accessible ($SWAGGER_STATUS)"
fi
# Check security headers
check_header "X-Content-Type-Options" "nosniff"
check_header "X-Frame-Options" "DENY"
check_header "Strict-Transport-Security" "max-age"
check_header "Content-Security-Policy" "default-src"
# Check server info không bị lộ
SERVER=$(curl -sI "$API_URL/health" | grep -i "^Server:" | tr -d '\r')
if [ -n "$SERVER" ]; then
echo "❌ Server header exposed: $SERVER"
FAILED=1
else
echo "✅ OK: Server header not exposed"
fi
exit $FAILED
Kết
Security Misconfiguration nhảy lên #2 OWASP vì nó là loại lỗi "vô hình" — code đúng nhưng config sai, thường là sai ở boundary giữa development và production. Tự động hóa check quan trọng hơn manual review ở đây: header check script, Swagger URL probe, và CORS origin validation nên là một phần của CD pipeline.
Bài cuối cùng trong series: Dependency confusion attack — sâu hơn vào một vector cụ thể trong supply chain, khi package nội bộ bị shadow bởi public package cùng tên và cách ngăn trong NuGet ecosystem.
Thiết Kiếm — BKGlobal Tech Team
#BKGlobal #appsec #security #owasp #misconfiguration #dotnet #aspnetcore