fix: harden recipe content against stored XSS (chips, instructions, asset media) (#7719)

This commit is contained in:
Hayden
2026-05-31 11:14:16 -05:00
committed by GitHub
parent 48752bcd06
commit 2d8b74282a
15 changed files with 362 additions and 49 deletions

View File

@@ -0,0 +1,54 @@
<template>
<div>
<p>
To harden Mealie against malicious content, <code>&lt;iframe&gt;</code> embeds in recipe
instructions, notes, and descriptions are now restricted to a trusted set of hosts.
</p>
<div class="mb-2">
By default, embeds are allowed only from well-known video providers:
<ul class="ml-6">
<li>YouTube</li>
<li>Vimeo</li>
</ul>
</div>
<p>
Existing recipes that embed content from <strong>other</strong> hosts will no longer render
those embeds. The rest of the recipe is unaffected.
</p>
<div v-if="user?.admin">
<hr class="mt-2 mb-4">
<p>
As an admin, you can allow additional hosts with the <code>ALLOWED_IFRAME_HOSTS</code>
environment variable (comma-separated). It extends the built-in defaults, and only
<code>https</code> sources are permitted. See the configuration docs for details:
<br>
<v-btn
class="mt-2"
color="primary"
href="https://docs.mealie.io/documentation/getting-started/installation/backend-config/"
target="_blank"
>
Backend Configuration
</v-btn>
</p>
</div>
</div>
</template>
<script setup lang="ts">
import type { AnnouncementMeta } from "~/composables/use-announcements";
const { user } = useMealieAuth();
</script>
<script lang="ts">
export const meta: AnnouncementMeta = {
title: "Recipe embeds restricted to trusted hosts",
};
</script>
<style scoped lang="css">
p {
padding-bottom: 8px;
}
</style>

View File

@@ -25,6 +25,7 @@
<script setup lang="ts">
import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
import { truncateText as truncatePlainText } from "~/lib/sanitize/text";
export type UrlPrefixParam = "tags" | "categories" | "tools";
@@ -50,10 +51,7 @@ const props = withDefaults(defineProps<Props>(), {
defineEmits(["item-selected"]);
function truncateText(text: string, length = 20, clamp = "...") {
if (!props.truncate) return text;
const node = document.createElement("div");
node.innerHTML = text;
const content = node.textContent || "";
return content.length > length ? content.slice(0, length) + clamp : content;
return truncatePlainText(text, length, clamp);
}
</script>