fix(desktop): set referrerpolicy on youtube embed iframes (#5039)
This commit is contained in:
parent
de935145da
commit
c3e1c26194
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
## No longer broken
|
||||
|
||||
- Fixed YouTube embeds in entry content and readability view failing to play due to a missing referrer
|
||||
|
||||
## Thanks
|
||||
|
||||
Special thanks to volunteer contributors @ for their valuable contributions
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { renderToString } from "react-dom/server"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { extractCodeFromHtml } from "../parse-html"
|
||||
import { extractCodeFromHtml, parseHtml } from "../parse-html"
|
||||
|
||||
describe("extractCodeFromHtml", () => {
|
||||
it("should extract code from div elements", () => {
|
||||
|
|
@ -388,3 +389,35 @@ describe("extractCodeFromHtml", () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseHtml iframe", () => {
|
||||
const renderIframe = (attrs: string) =>
|
||||
renderToString(parseHtml(`<iframe ${attrs}></iframe>`).toContent()).toLowerCase()
|
||||
|
||||
const youtubeSrc = `src="https://www.youtube.com/embed/dQw4w9WgXcQ"`
|
||||
|
||||
it("should rewrite referrer-hiding policies on youtube iframes", () => {
|
||||
for (const policy of ["no-referrer", "same-origin"]) {
|
||||
expect(renderIframe(`${youtubeSrc} referrerpolicy="${policy}"`)).toContain(
|
||||
`referrerpolicy="strict-origin-when-cross-origin"`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("should keep other declared referrerpolicy values on youtube iframes", () => {
|
||||
expect(renderIframe(`${youtubeSrc} referrerpolicy="origin"`)).toContain(
|
||||
`referrerpolicy="origin"`,
|
||||
)
|
||||
})
|
||||
|
||||
it("should set referrerpolicy when youtube iframes declare none", () => {
|
||||
// an absent attribute would inherit no-referrer from the document meta tag
|
||||
expect(renderIframe(youtubeSrc)).toContain(`referrerpolicy="strict-origin-when-cross-origin"`)
|
||||
})
|
||||
|
||||
it("should drop referrerpolicy on non-youtube iframes", () => {
|
||||
expect(
|
||||
renderIframe(`src="https://example.com/embed" referrerpolicy="no-referrer"`),
|
||||
).not.toContain("referrerpolicy")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import { createHeadingRenderer } from "~/components/ui/markdown/renderers/Headin
|
|||
import { MarkdownInlineImage } from "~/components/ui/markdown/renderers/InlineImage"
|
||||
import { Media } from "~/components/ui/media/Media"
|
||||
|
||||
const youtubeEmbedRegex = /^https:\/\/(?:www\.)?(?:youtube\.com|youtube-nocookie\.com)\/embed\//
|
||||
|
||||
function markInlineImage(node?: Element) {
|
||||
for (const item of node?.children ?? []) {
|
||||
if (item.type === "element" && item.tagName === "img") {
|
||||
|
|
@ -142,7 +144,7 @@ export const parseHtml = (
|
|||
return createElement("input", props)
|
||||
},
|
||||
iframe: ({ node, ...props }) => {
|
||||
const { width, height, src, ...rest } = props
|
||||
const { width, height, src, referrerPolicy, ...rest } = props
|
||||
|
||||
// Apply security sandbox attributes and responsive styling
|
||||
return createElement("iframe", {
|
||||
|
|
@ -154,6 +156,16 @@ export const parseHtml = (
|
|||
sandbox: "allow-scripts allow-same-origin allow-popups allow-forms",
|
||||
allowFullScreen: true,
|
||||
loading: "lazy",
|
||||
// Avoid YouTube Error 153 https://developers.google.com/youtube/terms/required-minimum-functionality#embedded-player-api-client-identity
|
||||
...(typeof src === "string" &&
|
||||
youtubeEmbedRegex.test(src) && {
|
||||
referrerPolicy:
|
||||
!referrerPolicy ||
|
||||
referrerPolicy === "no-referrer" ||
|
||||
referrerPolicy === "same-origin"
|
||||
? "strict-origin-when-cross-origin"
|
||||
: referrerPolicy,
|
||||
}),
|
||||
style: {
|
||||
aspectRatio: width && height ? `${width} / ${height}` : "16 / 9",
|
||||
...rest.style,
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ export const parseHtml = (content: string, options?: ParseHtmlOptions) => {
|
|||
"allowfullscreen",
|
||||
"sandbox",
|
||||
"loading",
|
||||
"referrerPolicy",
|
||||
"title",
|
||||
"id",
|
||||
"class",
|
||||
|
|
|
|||
|
|
@ -22,4 +22,18 @@ describe("sanitizeHTMLString", () => {
|
|||
expect(clean).toContain('<img src="/image.png">')
|
||||
expect(clean).toContain("<a>link</a>")
|
||||
})
|
||||
|
||||
it("keeps youtube embed iframes and strips any other iframe", () => {
|
||||
const clean = sanitizeHTMLString(`
|
||||
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="640" height="360"></iframe>
|
||||
<iframe src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"></iframe>
|
||||
<iframe src="https://evil.example.com/embed"></iframe>
|
||||
<iframe src="https://www.youtube.com/watch?v=dQw4w9WgXcQ"></iframe>
|
||||
`)
|
||||
|
||||
expect(clean).toContain("https://www.youtube.com/embed/dQw4w9WgXcQ")
|
||||
expect(clean).toContain("https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ")
|
||||
expect(clean).not.toContain("evil.example.com")
|
||||
expect(clean).not.toContain("watch?v=")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import DOMPurify from "dompurify"
|
||||
import { JSDOM } from "jsdom"
|
||||
|
||||
const youtubeEmbedRegex = /^https:\/\/(?:www\.)?(?:youtube\.com|youtube-nocookie\.com)\/embed\//
|
||||
|
||||
// For avoiding xss attack from readability, the raw document string should be sanitized.
|
||||
// The xss attack in electron may lead to more serious outcomes than browser environment.
|
||||
// It may allow remote execution of malicious scripts in the main process.
|
||||
|
|
@ -11,5 +13,15 @@ export function sanitizeHTMLString(dirtyDocumentString: string) {
|
|||
throw new Error("DOMPurify is not supported in the current DOM environment.")
|
||||
}
|
||||
|
||||
return purify.sanitize(dirtyDocumentString)
|
||||
// Keep YouTube embed iframes; any other iframe is still stripped.
|
||||
purify.addHook("uponSanitizeElement", (node, data) => {
|
||||
if (
|
||||
data.tagName === "iframe" &&
|
||||
!youtubeEmbedRegex.test((node as Element).getAttribute?.("src") ?? "")
|
||||
) {
|
||||
node.parentNode?.removeChild(node)
|
||||
}
|
||||
})
|
||||
|
||||
return purify.sanitize(dirtyDocumentString, { ADD_TAGS: ["iframe"] })
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue