Sécurité applicative
La sécurité est un aspect fondamental du développement d'applications. Cet article traite de Intégration Continue avec GitHub Actions et des meilleures pratiques pour sécuriser vos applications ASP.NET Core.
Authentification et autorisation
Configuration de l'authentification JWT
// Configuration dans Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]))
};
});
// Service de génération de tokens
public class TokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config)
{
_config = config;
}
public string GenerateToken(ApplicationUser user, IList<string> roles)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_config["Jwt:SecretKey"]);
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Email, user.Email)
};
// Ajouter les rôles
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(24),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature),
Issuer = _config["Jwt:Issuer"],
Audience = _config["Jwt:Audience"]
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
Autorisation basée sur les politiques
// Configuration des politiques
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("AuthorOrAdmin", policy =>
policy.RequireRole("Author", "Admin"));
options.AddPolicy("MinimumAge", policy =>
policy.RequireClaim("birthdate", birthdate =>
DateTime.Today.Year - DateTime.Parse(birthdate).Year >= 18));
// Autorisation basée sur les ressources
options.AddPolicy("OwnerPolicy", policy =>
policy.Requirements.Add(new OwnerRequirement()));
});
// Handler personnalisé pour l'autorisation
public class OwnerAuthorizationHandler : AuthorizationHandler<OwnerRequirement, Article>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OwnerRequirement requirement,
Article resource)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId == resource.AuthorId || context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Protection contre les attaques
Validation et sanitisation des données
// Modèle avec validation
public class SecureArticleDto
{
[Required]
[StringLength(200, MinimumLength = 5)]
[RegularExpression(@"^[a-zA-ZÀ-ÿ0-9\s\-_]+$",
ErrorMessage = "Le titre contient des caractères non autorisés")]
public string Title { get; set; }
[Required]
[StringLength(5000)]
public string Content { get; set; }
[EmailAddress]
public string? AuthorEmail { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "ID de catégorie invalide")]
public int CategoryId { get; set; }
}
// Service de sanitisation
public class InputSanitizer
{
private readonly HtmlSanitizer _sanitizer;
public InputSanitizer()
{
_sanitizer = new HtmlSanitizer();
_sanitizer.AllowedTags.Clear();
_sanitizer.AllowedTags.Add("p");
_sanitizer.AllowedTags.Add("br");
_sanitizer.AllowedTags.Add("strong");
_sanitizer.AllowedTags.Add("em");
_sanitizer.AllowedTags.Add("ul");
_sanitizer.AllowedTags.Add("ol");
_sanitizer.AllowedTags.Add("li");
}
public string SanitizeHtml(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
return _sanitizer.Sanitize(input);
}
public string EscapeForSql(string input)
{
return input?.Replace("'", "''") ?? string.Empty;
}
}
Protection CSRF et en-têtes de sécurité
// Configuration des en-têtes de sécurité
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
context.Response.Headers.Add("X-Frame-Options", "DENY");
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
context.Response.Headers.Add("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
await next();
});
// Protection CSRF
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.Cookie.Name = "__Secure-CSRF-Token";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> CreateArticle([FromBody] SecureArticleDto model)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Traitement sécurisé...
return Ok();
}
Gestion sécurisée des données sensibles
// Service de chiffrement
public class EncryptionService
{
private readonly string _encryptionKey;
public EncryptionService(IConfiguration config)
{
_encryptionKey = config["Encryption:Key"];
}
public string Encrypt(string plainText)
{
using var aes = Aes.Create();
aes.Key = Convert.FromBase64String(_encryptionKey);
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor();
using var ms = new MemoryStream();
using var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write);
using var writer = new StreamWriter(cs);
writer.Write(plainText);
writer.Close();
var encrypted = ms.ToArray();
var result = new byte[aes.IV.Length + encrypted.Length];
Array.Copy(aes.IV, 0, result, 0, aes.IV.Length);
Array.Copy(encrypted, 0, result, aes.IV.Length, encrypted.Length);
return Convert.ToBase64String(result);
}
public string Decrypt(string cipherText)
{
var buffer = Convert.FromBase64String(cipherText);
using var aes = Aes.Create();
aes.Key = Convert.FromBase64String(_encryptionKey);
var iv = new byte[aes.IV.Length];
var encrypted = new byte[buffer.Length - iv.Length];
Array.Copy(buffer, 0, iv, 0, iv.Length);
Array.Copy(buffer, iv.Length, encrypted, 0, encrypted.Length);
aes.IV = iv;
using var decryptor = aes.CreateDecryptor();
using var ms = new MemoryStream(encrypted);
using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read);
using var reader = new StreamReader(cs);
return reader.ReadToEnd();
}
}
Audit et logging sécurisé
// Service d'audit
public class SecurityAuditService
{
private readonly ILogger<SecurityAuditService> _logger;
public SecurityAuditService(ILogger<SecurityAuditService> logger)
{
_logger = logger;
}
public void LogSecurityEvent(string userId, string action, string resource, bool success)
{
_logger.LogWarning("Security Event: User {UserId} attempted {Action} on {Resource}. Success: {Success}",
userId, action, resource, success);
}
public void LogSuspiciousActivity(string ipAddress, string userAgent, string details)
{
_logger.LogCritical("Suspicious Activity: IP {IP}, UserAgent {UserAgent}, Details: {Details}",
ipAddress, userAgent, details);
}
}
Conclusion
La sécurité avec Intégration Continue avec GitHub Actions doit être intégrée dès la conception. Les mesures présentées - authentification robuste, autorisation granulaire, protection contre les attaques et audit - constituent les fondamentaux d'une application sécurisée. Restez vigilant et mettez régulièrement à jour vos connaissances en sécurité.