fix(recipe): add server-side image size limit and use .matches() for type check

- @Size(max=7_000_000) on heroImageUrl enforces ~5 MB cap at bean validation
- ALLOWED_IMAGE_PATTERN uses .matches() for unambiguous full-string check
- Tests: oversized image → 400, empty ingredients list → 400

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 09:27:35 +02:00
parent f11cca534f
commit ed769b18a4
3 changed files with 30 additions and 3 deletions

View File

@@ -191,11 +191,11 @@ public class RecipeService {
// ── Image validation ──
private static final java.util.regex.Pattern ALLOWED_IMAGE_PATTERN =
java.util.regex.Pattern.compile("^data:image/(jpeg|jpg|png|gif|webp);base64,");
java.util.regex.Pattern.compile("data:image/(jpeg|jpg|png|gif|webp);base64,.*");
private void validateHeroImageUrl(String heroImageUrl) {
if (heroImageUrl == null || heroImageUrl.isBlank()) return;
if (!ALLOWED_IMAGE_PATTERN.matcher(heroImageUrl).find()) {
if (!ALLOWED_IMAGE_PATTERN.matcher(heroImageUrl).matches()) {
throw new ValidationException("Ungültiger Bildtyp. Erlaubt sind: JPEG, PNG, GIF, WebP.");
}
}

View File

@@ -12,7 +12,7 @@ public record RecipeCreateRequest(
Integer serves,
Integer cookTimeMin,
@NotBlank @Pattern(regexp = "easy|medium|hard") String effort,
String heroImageUrl,
@Size(max = 7_000_000) String heroImageUrl,
@NotEmpty @Valid List<IngredientEntry> ingredients,
@Valid List<StepEntry> steps,
@NotEmpty List<UUID> tagIds

View File

@@ -161,6 +161,33 @@ class RecipeControllerTest {
verify(recipeService).deleteRecipe(HOUSEHOLD_ID, RECIPE_ID);
}
@Test
void createRecipeWithOversizedHeroImageShouldReturn400() throws Exception {
String heroImageUrl = "data:image/jpeg;base64," + "A".repeat(7_000_000);
String body = "{\"name\":\"Test\",\"effort\":\"easy\",\"tagIds\":[\"" + UUID.randomUUID() + "\"]," +
"\"ingredients\":[{\"quantity\":1,\"unit\":\"g\",\"newIngredientName\":\"x\",\"sortOrder\":0}]," +
"\"heroImageUrl\":\"" + heroImageUrl + "\"}";
mockMvc.perform(post("/v1/recipes")
.principal(() -> "sarah@example.com")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest());
}
@Test
void createRecipeWithEmptyIngredientsListShouldReturn400() throws Exception {
var body = """
{"name":"Test","effort":"easy","tagIds":["%s"],"ingredients":[]}
""".formatted(UUID.randomUUID());
mockMvc.perform(post("/v1/recipes")
.principal(() -> "sarah@example.com")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest());
}
@Test
void createRecipeWithCapitalisedEffortShouldReturn400() throws Exception {
var body = """