escape all meta content

This commit is contained in:
Michael Genson 2025-07-20 00:28:54 +00:00
commit f3383ac420

View file

@ -1,6 +1,8 @@
import html
import json import json
import pathlib import pathlib
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from fastapi import Depends, FastAPI, Response from fastapi import Depends, FastAPI, Response
@ -24,6 +26,9 @@ class MetaTag:
property_name: str property_name: str
content: str content: str
def __post_init__(self):
self.content = escape(self.content) # escape HTML to prevent XSS attacks
class SPAStaticFiles(StaticFiles): class SPAStaticFiles(StaticFiles):
async def get_response(self, path: str, scope): async def get_response(self, path: str, scope):
@ -42,6 +47,17 @@ __app_settings = get_app_settings()
__contents = "" __contents = ""
def escape(content: Any) -> Any:
if isinstance(content, str):
return html.escape(content)
elif isinstance(content, list | tuple | set):
return [escape(item) for item in content]
elif isinstance(content, dict):
return {escape(k): escape(v) for k, v in content.items()}
else:
return content
def inject_meta(contents: str, tags: list[MetaTag]) -> str: def inject_meta(contents: str, tags: list[MetaTag]) -> str:
soup = BeautifulSoup(contents, "lxml") soup = BeautifulSoup(contents, "lxml")
scraped_meta_tags = soup.find_all("meta") scraped_meta_tags = soup.find_all("meta")
@ -80,15 +96,13 @@ def content_with_meta(group_slug: str, recipe: Recipe) -> str:
# Inject meta tags # Inject meta tags
recipe_url = f"{__app_settings.BASE_URL}/g/{group_slug}/r/{recipe.slug}" recipe_url = f"{__app_settings.BASE_URL}/g/{group_slug}/r/{recipe.slug}"
if recipe.image: if recipe.image:
image_url = ( image_url = f"{__app_settings.BASE_URL}/api/media/recipes/{recipe.id}/images/original.webp?version={escape(recipe.image)}"
f"{__app_settings.BASE_URL}/api/media/recipes/{recipe.id}/images/original.webp?version={recipe.image}"
)
else: else:
image_url = "https://raw.githubusercontent.com/mealie-recipes/mealie/9571816ac4eed5beacfc0abf6c03eff1427fd0eb/frontend/static/icons/android-chrome-512x512.png" image_url = "https://raw.githubusercontent.com/mealie-recipes/mealie/9571816ac4eed5beacfc0abf6c03eff1427fd0eb/frontend/static/icons/android-chrome-512x512.png"
ingredients: list[str] = [] ingredients: list[str] = []
if recipe.settings.disable_amount: # type: ignore if recipe.settings.disable_amount: # type: ignore
ingredients = [i.note for i in recipe.recipe_ingredient if i.note] ingredients = [escape(i.note) for i in recipe.recipe_ingredient if i.note]
else: else:
for ing in recipe.recipe_ingredient: for ing in recipe.recipe_ingredient:
@ -102,25 +116,30 @@ def content_with_meta(group_slug: str, recipe: Recipe) -> str:
if ing.note: if ing.note:
s += f"{ing.note}" s += f"{ing.note}"
ingredients.append(s) ingredients.append(escape(s))
nutrition: dict[str, str | None] = recipe.nutrition.model_dump(by_alias=True) if recipe.nutrition else {} nutrition: dict[str, str | None] = recipe.nutrition.model_dump(by_alias=True) if recipe.nutrition else {}
for k, v in nutrition.items():
if v:
nutrition[k] = escape(v)
as_schema_org = { as_schema_org: dict[str, Any] = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Recipe", "@type": "Recipe",
"name": recipe.name, "name": escape(recipe.name),
"description": recipe.description, "description": escape(recipe.description),
"image": [image_url], "image": [image_url],
"datePublished": recipe.created_at, "datePublished": recipe.created_at,
"prepTime": recipe.prep_time, "prepTime": escape(recipe.prep_time),
"cookTime": recipe.cook_time, "cookTime": escape(recipe.cook_time),
"totalTime": recipe.total_time, "totalTime": escape(recipe.total_time),
"recipeYield": recipe.recipe_yield_display, "recipeYield": escape(recipe.recipe_yield_display),
"recipeIngredient": ingredients, "recipeIngredient": ingredients,
"recipeInstructions": [i.text for i in recipe.recipe_instructions] if recipe.recipe_instructions else [], "recipeInstructions": [escape(i.text) for i in recipe.recipe_instructions]
"recipeCategory": [c.name for c in recipe.recipe_category] if recipe.recipe_category else [], if recipe.recipe_instructions
"keywords": [t.name for t in recipe.tags] if recipe.tags else [], else [],
"recipeCategory": [escape(c.name) for c in recipe.recipe_category] if recipe.recipe_category else [],
"keywords": [escape(t.name) for t in recipe.tags] if recipe.tags else [],
"nutrition": nutrition, "nutrition": nutrition,
} }