fix(server): use `xss` to serialize data

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2024-10-30 15:43:37 +08:00
parent 81d49f0893
commit 93f58ecfd8
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
4 changed files with 34 additions and 53 deletions

View File

@ -34,7 +34,8 @@
"satori": "0.11.2",
"sonner": "^1.5.0",
"tailwindcss": "3.4.14",
"use-context-selector": "2.0.0"
"use-context-selector": "2.0.0",
"xss": "1.0.15"
},
"devDependencies": {
"@follow/components": "workspace:*",
@ -45,14 +46,12 @@
"@follow/types": "workspace:*",
"@follow/utils": "workspace:*",
"@radix-ui/react-avatar": "1.1.1",
"@types/serialize-javascript": "5.0.4",
"daisyui": "4.12.13",
"foxact": "0.2.39",
"lightningcss": "1.27.0",
"lodash-es": "4.17.21",
"masonic": "4.0.1",
"react-dom": "^18.3.1",
"serialize-javascript": "6.0.2",
"tailwindcss": "3.4.14",
"tsup": "8.3.0",
"tsx": "4.19.1",

View File

@ -1,18 +1,4 @@
import serialize from "serialize-javascript"
function escapeHtml(unsafe?: string | null) {
if (!unsafe) {
return unsafe
}
return unsafe
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#x27;")
.replaceAll("/", "&#x2F;")
}
import xss from "xss"
export function buildSeoMetaTags(configs: {
openGraph: {
@ -22,25 +8,23 @@ export function buildSeoMetaTags(configs: {
}
}) {
const openGraph = {
title: escapeHtml(configs.openGraph.title),
description: escapeHtml(configs.openGraph.description),
image: escapeHtml(configs.openGraph.image),
title: xss(configs.openGraph.title),
description: xss(configs.openGraph.description ?? ""),
image: xss(configs.openGraph.image ?? ""),
}
const title = `${openGraph.title} | Follow`
return [
`<meta property="og:title" content="${serialize(title)}" />`,
`<meta property="og:title" content="${title}" />`,
openGraph.description
? `<meta property="og:description" content="${serialize(openGraph.description)}" />`
? `<meta property="og:description" content="${openGraph.description}" />`
: "",
openGraph.image ? `<meta property="og:image" content="${serialize(openGraph.image)}" />` : "",
openGraph.image ? `<meta property="og:image" content="${openGraph.image}" />` : "",
// Twitter
`<meta property="twitter:card" content="summary_large_image" />`,
`<meta property="twitter:title" content="${serialize(title)}" />`,
`<meta property="twitter:title" content="${title}" />`,
openGraph.description
? `<meta property="twitter:description" content="${serialize(openGraph.description)}" />`
: "",
openGraph.image
? `<meta property="twitter:image" content="${serialize(openGraph.image)}" />`
? `<meta property="twitter:description" content="${openGraph.description}" />`
: "",
openGraph.image ? `<meta property="twitter:image" content="${openGraph.image}" />` : "",
].join("\n")
}

View File

@ -6,7 +6,7 @@ import { env } from "@follow/shared/env"
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"
import { parseHTML } from "linkedom"
import { FetchError } from "ofetch"
import serialize from "serialize-javascript"
import xss from "xss"
import { isDev } from "~/lib/env"
import { buildSeoMetaTags } from "~/lib/seo"
@ -119,14 +119,12 @@ async function injectMetaToTemplate(template: string, req: FastifyRequest) {
break
}
case "meta": {
allMetaString.push(
`<meta property="${meta.property}" content="${serialize(meta.content)}" />`,
)
allMetaString.push(`<meta property="${meta.property}" content="${xss(meta.content)}" />`)
break
}
case "title": {
if (meta.title) {
template = template.replace(`<!-- TITLE -->`, `${serialize(meta.title)} | Follow`)
template = template.replace(`<!-- TITLE -->`, `${xss(meta.title)} | Follow`)
isTitleReplaced = true
}
break
@ -137,7 +135,7 @@ async function injectMetaToTemplate(template: string, req: FastifyRequest) {
const script = document.createElement("script")
script.innerHTML = `
window.__HYDRATE__ = window.__HYDRATE__ || {}
window.__HYDRATE__['${meta.key}'] = ${serialize(meta.data)}
window.__HYDRATE__['${meta.key}'] = ${xss(JSON.stringify(meta.data))}
`
document.head.append(script)
template = document.toString()

View File

@ -730,6 +730,9 @@ importers:
use-context-selector:
specifier: 2.0.0
version: 2.0.0(react@18.3.1)(scheduler@0.23.2)
xss:
specifier: 1.0.15
version: 1.0.15
devDependencies:
'@follow/components':
specifier: workspace:*
@ -755,9 +758,6 @@ importers:
'@radix-ui/react-avatar':
specifier: 1.1.1
version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/serialize-javascript':
specifier: 5.0.4
version: 5.0.4
daisyui:
specifier: 4.12.13
version: 4.12.13(patch_hash=igsntdatmoaxzwxof4bkkh35fy)(postcss@8.4.47)
@ -776,9 +776,6 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
serialize-javascript:
specifier: 6.0.2
version: 6.0.2
tsup:
specifier: 8.3.0
version: 8.3.0(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.3)(yaml@2.6.0)
@ -4383,9 +4380,6 @@ packages:
'@types/semver@7.5.8':
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
'@types/serialize-javascript@5.0.4':
resolution: {integrity: sha512-Z2R7UKFuNWCP8eoa2o9e5rkD3hmWxx/1L0CYz0k2BZzGh0PhEVMp9kfGiqEml/0IglwNERXZ2hwNzIrSz/KHTA==}
'@types/shimmer@1.2.0':
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
@ -5397,6 +5391,9 @@ packages:
engines: {node: '>=4'}
hasBin: true
cssfilter@0.0.10:
resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==}
cssnano-preset-default@7.0.6:
resolution: {integrity: sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==}
engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
@ -8992,9 +8989,6 @@ packages:
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
engines: {node: '>=10'}
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
@ -9970,6 +9964,11 @@ packages:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'}
xss@1.0.15:
resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==}
engines: {node: '>= 0.10.0'}
hasBin: true
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@ -14284,8 +14283,6 @@ snapshots:
'@types/semver@7.5.8': {}
'@types/serialize-javascript@5.0.4': {}
'@types/shimmer@1.2.0': {}
'@types/tar@6.1.13':
@ -15555,6 +15552,8 @@ snapshots:
cssesc@3.0.0: {}
cssfilter@0.0.10: {}
cssnano-preset-default@7.0.6(postcss@8.4.47):
dependencies:
browserslist: 4.23.3
@ -19876,10 +19875,6 @@ snapshots:
type-fest: 0.13.1
optional: true
serialize-javascript@6.0.2:
dependencies:
randombytes: 2.1.0
server-only@0.0.1: {}
set-blocking@2.0.0: {}
@ -20853,6 +20848,11 @@ snapshots:
xmlbuilder@15.1.1: {}
xss@1.0.15:
dependencies:
commander: 2.20.3
cssfilter: 0.0.10
xtend@4.0.2: {}
y18n@5.0.8: {}