Skip to content

Commit 2bc764f

Browse files
committed
fix: address code review findings
- Remove blocked URL from node.props.src to prevent Satori re-fetching - Coerce dimensions to Number before clamping (query params arrive as strings) - Clear setTimeout on successful render to prevent timer leak
1 parent 996428d commit 2bc764f

File tree

3 files changed

+17
-9
lines changed

3 files changed

+17
-9
lines changed

src/runtime/server/og-image/context.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,16 @@ export async function resolveContext(e: H3Event): Promise<H3Error | OgImageRende
143143
const ogImageRouteRules = separateProps(routeRules.ogImage as RouteRulesOgImage)
144144
const options = defu(queryParams, urlOptions, ogImageRouteRules, runtimeConfig.defaults) as OgImageOptionsInternal
145145

146-
// Clamp dimensions to prevent DoS via oversized image generation (GHSA-c7xp-q6q8-hg76)
146+
// Clamp dimensions to prevent DoS via oversized image generation
147147
const maxDim = runtimeConfig.security?.maxDimension || 2048
148-
if (typeof options.width === 'number')
149-
options.width = Math.min(Math.max(1, options.width), maxDim)
150-
if (typeof options.height === 'number')
151-
options.height = Math.min(Math.max(1, options.height), maxDim)
148+
if (options.width != null) {
149+
const w = Number(options.width)
150+
options.width = Number.isFinite(w) ? Math.min(Math.max(1, w), maxDim) : undefined
151+
}
152+
if (options.height != null) {
153+
const h = Number(options.height)
154+
options.height = Number.isFinite(h) ? Math.min(Math.max(1, h), maxDim) : undefined
155+
}
152156

153157
// Strip HTML event handlers and dangerous attributes from props (GHSA-mg36-wvcr-m75h)
154158
if (options.props && typeof options.props === 'object')

src/runtime/server/og-image/core/plugins/imageSrc.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,13 @@ export default defineTransformer([
135135
// avoid trying to fetch base64 image uris
136136
else if (!src.startsWith('data:')) {
137137
src = decodeHtml(src)
138-
node.props.src = src
139138
// Block private/loopback URLs outside dev to prevent SSRF
140139
if (!import.meta.dev && isBlockedUrl(src)) {
141140
logger.warn(`Blocked internal image fetch: ${src}`)
141+
delete node.props.src
142142
}
143143
else {
144+
node.props.src = src
144145
// fetch remote images and embed as base64 to avoid satori re-fetching at render time
145146
imageBuffer = (await $fetch(src, {
146147
responseType: 'arrayBuffer',

src/runtime/server/util/eventHandlers.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,21 @@ export async function imageEventHandler(e: H3Event) {
122122
if (!image) {
123123
const { security } = useOgImageRuntimeConfig()
124124
const timeout = security?.renderTimeout || 15_000
125+
let timer: ReturnType<typeof setTimeout> | undefined
125126
image = await Promise.race([
126127
renderer.createImage(ctx),
127-
new Promise<never>((_, reject) =>
128-
setTimeout(() => reject(new Error(`OG image render timed out after ${timeout}ms`)), timeout),
129-
),
128+
new Promise<never>((_, reject) => {
129+
timer = setTimeout(() => reject(new Error(`OG image render timed out after ${timeout}ms`)), timeout)
130+
}),
130131
]).catch((err: any) => {
131132
if (err?.message?.includes('timed out')) {
132133
logger.error(`renderer.createImage timeout for ${e.path}`)
133134
return createError({ statusCode: 408, statusMessage: `[Nuxt OG Image] Render timed out.` })
134135
}
135136
logger.error(`renderer.createImage error for ${e.path}:`, err?.stack || err?.message || err)
136137
throw err
138+
}).finally(() => {
139+
clearTimeout(timer)
137140
})
138141
if (image instanceof H3Error)
139142
return image

0 commit comments

Comments
 (0)