feat(planner): sanitize heroImageUrl before embedding in CSS url()
Extracts sanitizeForCssUrl helper that strips '"()\ before the URL
is embedded in url("..."). Prevents CSS injection via the hero image
field in inline style bindings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import EmptyDayTile from './EmptyDayTile.svelte';
|
||||
import { formatDayAbbr } from '$lib/planner/week';
|
||||
import type { Recipe, Slot, Suggestion } from '$lib/planner/types';
|
||||
import { sanitizeForCssUrl } from '$lib/planner/DesktopDayTile.utils';
|
||||
|
||||
let {
|
||||
slot,
|
||||
@@ -67,7 +68,7 @@
|
||||
|
||||
const gradientBackground = $derived((() => {
|
||||
if (!slot.recipe) return 'var(--color-surface)';
|
||||
if (slot.recipe.heroImageUrl) return `url(${slot.recipe.heroImageUrl})`;
|
||||
if (slot.recipe.heroImageUrl) return `url("${sanitizeForCssUrl(slot.recipe.heroImageUrl)}")`;
|
||||
const proteinTag = slot.recipe.tags?.find((t) => t.tagType === 'protein');
|
||||
if (proteinTag?.name) {
|
||||
return `var(--gradient-protein-${toCssKey(proteinTag.name)})`;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
import DesktopDayTile from './DesktopDayTile.svelte';
|
||||
import { sanitizeForCssUrl } from './DesktopDayTile.utils';
|
||||
|
||||
const filledSlot = {
|
||||
id: 's1',
|
||||
@@ -18,6 +19,29 @@ const filledSlot = {
|
||||
|
||||
const emptySlot = { id: null, slotDate: '2026-04-14', recipe: null };
|
||||
|
||||
describe('sanitizeForCssUrl', () => {
|
||||
it('strips parentheses that could break out of url() context', () => {
|
||||
expect(sanitizeForCssUrl('x);}body{display:none}/*')).not.toContain(')');
|
||||
});
|
||||
|
||||
it('strips single quotes', () => {
|
||||
expect(sanitizeForCssUrl("data:image/png;base64,abc'def")).not.toContain("'");
|
||||
});
|
||||
|
||||
it('strips double quotes', () => {
|
||||
expect(sanitizeForCssUrl('data:image/png;base64,abc"def')).not.toContain('"');
|
||||
});
|
||||
|
||||
it('strips backslashes', () => {
|
||||
expect(sanitizeForCssUrl('data:image/png;base64,abc\\def')).not.toContain('\\');
|
||||
});
|
||||
|
||||
it('preserves a safe data URI unchanged', () => {
|
||||
const safe = 'data:image/png;base64,abc123+/==';
|
||||
expect(sanitizeForCssUrl(safe)).toBe(safe);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DesktopDayTile — filled slot', () => {
|
||||
describe('front face', () => {
|
||||
it('renders recipe name on front face', () => {
|
||||
|
||||
8
frontend/src/lib/planner/DesktopDayTile.utils.ts
Normal file
8
frontend/src/lib/planner/DesktopDayTile.utils.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Strips characters that could break out of a CSS url() context or inject
|
||||
* CSS into an inline style attribute. Safe data URIs (base64) are unaffected
|
||||
* as they contain only A-Z, a-z, 0-9, +, /, = and the data: prefix.
|
||||
*/
|
||||
export function sanitizeForCssUrl(url: string): string {
|
||||
return url.replace(/['"()\\]/g, '');
|
||||
}
|
||||
Reference in New Issue
Block a user