More security!
All checks were successful
Build Docker Images / test (map[name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m25s
Build Docker Images / test (map[name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m25s
Build Docker Images / build-and-push (map[image_suffix:morska name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m41s
Build Docker Images / build-and-push (map[image_suffix:pedrollopl name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m40s

This commit is contained in:
2025-12-05 23:41:56 +01:00
parent 0c874575d4
commit 595076033b
9 changed files with 131 additions and 7 deletions

View File

@@ -2,6 +2,7 @@ using DiunaBI.API.Services;
using DiunaBI.Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
namespace DiunaBI.API.Controllers;
@@ -15,6 +16,7 @@ public class AuthController(
: ControllerBase
{
[HttpPost("apiToken")]
[EnableRateLimiting("auth")]
public async Task<IActionResult> ApiToken([FromBody] string idToken)
{
try

View File

@@ -64,10 +64,20 @@ public class DataInboxController : Controller
}
// check if datainbox.data is base64 encoded value
if (!string.IsNullOrEmpty(dataInbox.Data) && !IsBase64String(dataInbox.Data))
if (!string.IsNullOrEmpty(dataInbox.Data))
{
_logger.LogWarning("DataInbox: Invalid data format - not base64 encoded for source {Source}", dataInbox.Source);
return BadRequest("Invalid data format - not base64 encoded");
// Limit data size to 10MB to prevent DoS
if (dataInbox.Data.Length > 10_000_000)
{
_logger.LogWarning("DataInbox: Data too large for source {Source}, size {Size}", dataInbox.Source, dataInbox.Data.Length);
return BadRequest("Data too large (max 10MB)");
}
if (!IsBase64String(dataInbox.Data))
{
_logger.LogWarning("DataInbox: Invalid data format - not base64 encoded for source {Source}", dataInbox.Source);
return BadRequest("Invalid data format - not base64 encoded");
}
}
dataInbox.Id = Guid.NewGuid();
@@ -97,6 +107,16 @@ public class DataInboxController : Controller
{
try
{
// Validate pagination parameters
if (limit <= 0 || limit > 1000)
{
return BadRequest("Limit must be between 1 and 1000");
}
if (start < 0)
{
return BadRequest("Start must be non-negative");
}
var query = _db.DataInbox.AsQueryable();
if (!string.IsNullOrEmpty(search))

View File

@@ -42,6 +42,16 @@ public class JobsController : Controller
{
try
{
// Validate pagination parameters
if (limit <= 0 || limit > 1000)
{
return BadRequest("Limit must be between 1 and 1000");
}
if (start < 0)
{
return BadRequest("Start must be non-negative");
}
var query = _db.QueueJobs.AsQueryable();
if (status.HasValue)

View File

@@ -48,6 +48,16 @@ public class LayersController : Controller
{
try
{
// Validate pagination parameters
if (limit <= 0 || limit > 1000)
{
return BadRequest("Limit must be between 1 and 1000");
}
if (start < 0)
{
return BadRequest("Start must be non-negative");
}
var query = _db.Layers.Where(x => !x.IsDeleted);
if (name != null)

View File

@@ -1,9 +1,11 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using System.Text;
using System.Threading.RateLimiting;
using DiunaBI.API.Hubs;
using DiunaBI.API.Services;
using DiunaBI.Infrastructure.Data;
@@ -72,6 +74,41 @@ builder.Services.AddCors(options =>
builder.Services.AddControllers();
// Rate Limiting
builder.Services.AddRateLimiter(options =>
{
// Global API rate limit
options.AddFixedWindowLimiter("api", config =>
{
config.PermitLimit = 100;
config.Window = TimeSpan.FromMinutes(1);
config.QueueProcessingOrder = System.Threading.RateLimiting.QueueProcessingOrder.OldestFirst;
config.QueueLimit = 0; // No queueing
});
// Strict limit for authentication endpoint
options.AddFixedWindowLimiter("auth", config =>
{
config.PermitLimit = 10;
config.Window = TimeSpan.FromMinutes(1);
config.QueueProcessingOrder = System.Threading.RateLimiting.QueueProcessingOrder.OldestFirst;
config.QueueLimit = 0;
});
// Rejection response
options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = 429; // Too Many Requests
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "Too many requests. Please try again later.",
retryAfter = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)
? (double?)retryAfter.TotalSeconds
: (double?)null
}, cancellationToken: token);
};
});
// SignalR
builder.Services.AddSignalR();
@@ -202,6 +239,18 @@ pluginManager.LoadPluginsFromDirectory(pluginsPath);
app.UseCors("CORSPolicy");
// Security Headers
app.Use(async (context, next) =>
{
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
context.Response.Headers.Append("X-Frame-Options", "DENY");
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
await next();
});
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();

View File

@@ -36,7 +36,7 @@ public class GoogleAuthService(AppDbContext context, IConfiguration configuratio
if (user == null)
{
_logger.LogError("User not found in DiunaBI database: {Email}", payload.Email);
return (false, null, "User not found in DiunaBI database");
return (false, null, "Authentication failed");
}
user.UserName = payload.Name;