diff --git a/Endpoints/EndpointHelpers.cs b/Endpoints/EndpointHelpers.cs index 5a94e54..b19581e 100644 --- a/Endpoints/EndpointHelpers.cs +++ b/Endpoints/EndpointHelpers.cs @@ -34,6 +34,16 @@ internal static class EndpointHelpers ? t[..Math.Min(t.Length, max)] : null; + public static bool IsValidImageUrl(string? url) + { + if (string.IsNullOrWhiteSpace(url)) return true; // empty is acceptable + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false; + if (uri.Scheme is not ("http" or "https")) return false; + var path = uri.AbsolutePath.ToLowerInvariant(); + return path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg") + || path.EndsWith(".gif") || path.EndsWith(".webp") || path.EndsWith(".avif"); + } + public static async Task IsAdmin(HttpContext ctx, AppDbContext db, IConfiguration config) { var player = await GetAuthenticatedPlayer(ctx, db); diff --git a/Endpoints/SuggestEndpoints.cs b/Endpoints/SuggestEndpoints.cs index 2db51c0..24c0334 100644 --- a/Endpoints/SuggestEndpoints.cs +++ b/Endpoints/SuggestEndpoints.cs @@ -51,6 +51,11 @@ public static class SuggestEndpoints return Results.BadRequest(new { error = "Name is required and must be <= 100 characters." }); } + if (!EndpointHelpers.IsValidImageUrl(request.ScreenshotUrl)) + { + return Results.BadRequest(new { error = "Screenshot URL must be http(s) and end with an image file extension." }); + } + var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db); if (player is null) return Results.Unauthorized(); @@ -118,6 +123,11 @@ public static class SuggestEndpoints return Results.BadRequest(new { error = "Name is required and must be <= 100 characters." }); } + if (!EndpointHelpers.IsValidImageUrl(request.ScreenshotUrl)) + { + return Results.BadRequest(new { error = "Screenshot URL must be http(s) and end with an image file extension." }); + } + var suggestion = await db.Suggestions.FirstOrDefaultAsync(s => s.Id == id); if (suggestion == null) return Results.NotFound(new { error = "Suggestion not found." }); diff --git a/wwwroot/app.js b/wwwroot/app.js index 018767c..1be9eef 100644 --- a/wwwroot/app.js +++ b/wwwroot/app.js @@ -296,6 +296,9 @@ function setupHandlers() { const form = e.target; const data = Object.fromEntries(new FormData(form).entries()); if (!data.name) return toast("Name required", true); + if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) { + return toast("Screenshot URL must be http(s) and end with an image file.", true); + } try { await api.createSuggestion(data); form.reset(); @@ -468,6 +471,9 @@ function openEditModal(s) { form?.addEventListener("submit", async (e) => { e.preventDefault(); const data = Object.fromEntries(new FormData(form).entries()); + if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) { + return toast("Screenshot URL must be http(s) and end with an image file.", true); + } if (!data.name?.trim()) return toast("Name required", true); try { await api.updateSuggestion(s.id, data); @@ -516,3 +522,16 @@ async function main() { } main(); + +function isValidImageUrl(url) { + if (!url) return true; + try { + const u = new URL(url); + const allowed = ["http:", "https:"]; + if (!allowed.includes(u.protocol)) return false; + const path = u.pathname.toLowerCase(); + return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif"].some(ext => path.endsWith(ext)); + } catch { + return false; + } +}