@dicebear/converter vulnerable to ncontrolled memory allocation via crafted SVG dimensions
Description
DiceBear is an avatar library for designers and developers. Prior to version 9.4.0, the ensureSize() function in @dicebear/converter read the width and height attributes from the input SVG to determine the output canvas size for rasterization (PNG, JPEG, WebP, AVIF). An attacker who can supply a crafted SVG with extremely large dimensions (e.g. width="999999999") could force the server to allocate excessive memory, leading to denial of service. This primarily affects server-side applications that pass untrusted or user-supplied SVGs to the converter's toPng(), toJpeg(), toWebp(), or toAvif() functions. Applications that only convert self-generated DiceBear avatars are not practically exploitable, but are still recommended to upgrade. This is fixed in version 9.4.0. The ensureSize() function no longer reads SVG attributes to determine output size. Instead, a new size option (default: 512, max: 2048) controls the output dimensions. Invalid values (NaN, negative, zero, Infinity) fall back to the default. If upgrading is not immediately possible, validate and sanitize the width and height attributes of any untrusted SVG input before passing it to the converter.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
DiceBear avatar converter in versions <9.4.0 allows DoS via crafted SVG with excessive dimensions causing memory exhaustion.
Vulnerability
Overview
CVE-2026-29112 affects @dicebear/converter versions prior to 9.4.0. The root cause lies in the ensureSize() function, which parsed the width and height attributes from user-supplied SVG inputs to determine the canvas size for rasterizing into formats like PNG or JPEG. By providing an SVG with extreme dimension values (e.g., width="999999999"), an attacker could force the server to allocate an excessive amount of memory, leading to denial of service [1][4].
Attack
Vector & Prerequisites
Exploitation requires the attacker to supply a malicious SVG to a server-side application that uses the converter's toPng(), toJpeg(), toWebp(), or toAvif() functions with untrusted or user-controlled SVGs. Applications that only convert self-generated DiceBear avatars are not practically exploitable, though upgrading is still recommended [1][4]. The attack does not require authentication if the service accepts public SVG uploads.
Impact
A successful attack leads to uncontrolled memory allocation, potentially crashing the service or degrading performance to the point of unavailability. This is a denial-of-service vulnerability typical of resource-exhaustion bugs in image processing libraries [1][4].
Mitigation
The issue is fixed in version 9.4.0, released March 2026. The fix introduces a new size option (default: 512, maximum: 2048) that replaces reading dimension attributes from the SVG. Invalid values (NaN, negative, zero, Infinity) fall back to the default. A temporary workaround is to validate and sanitize the width and height attributes of any untrusted SVG input before passing it to the converter [1][4]. The fix can be confirmed in the commit [2] that modified the toFormat function to accept an options object with a size parameter.
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@dicebear/converternpm | < 9.4.0 | 9.4.0 |
Affected products
2Patches
142a59eac46a3fix: prevent DoS via crafted SVG dimensions in @dicebear/converter
8 files changed · +144 −36
CHANGELOG.md+23 −0 modified@@ -1,5 +1,28 @@ # Changelog +## 9.4.0 + +### Security + +- `@dicebear/converter`: `ensureSize()` no longer reads the SVG's `width` / + `height` attributes to determine the output canvas size. Previously, a + crafted SVG with extremely large dimensions could cause excessive memory + allocation (DoS). Thanks to [@maru1009](https://github.com/maru1009) for + reporting this issue. + +### New features + +- `@dicebear/converter`: New `size` option for `toPng`, `toJpeg`, `toWebp`, and + `toAvif` (default: 512, max: 2048). Invalid values (`NaN`, `<= 0`, + `Infinity`) fall back to 512. +- `dicebear` CLI: The `--size` flag is now forwarded to the converter, so + rasterized output matches the requested avatar size. + +### Breaking changes + +- `@dicebear/converter`: The output image size is no longer derived from the + input SVG dimensions. It is always set by the `size` option. + ## 9.0.0 This release fixes a compatibility issue with Next.js caused by the converter
packages/@dicebear/converter/src/core.ts+26 −24 modified@@ -6,58 +6,67 @@ import type { ToPng, ToWebp, ToAvif, + Options, } from './types.js'; import { getMimeType } from './utils/mime-type.js'; import { ensureSize } from './utils/svg.js'; -export const toPng: ToPng = (avatar: Avatar) => { - return toFormat(avatar, 'png'); +export const toPng: ToPng = (avatar: Avatar, options: Options = {}) => { + return toFormat(avatar, 'png', options); }; -export const toJpeg: ToJpeg = (avatar: Avatar) => { - return toFormat(avatar, 'jpeg'); +export const toJpeg: ToJpeg = (avatar: Avatar, options: Options = {}) => { + return toFormat(avatar, 'jpeg', options); }; -export const toWebp: ToWebp = (avatar: Avatar) => { - return toFormat(avatar, 'webp'); +export const toWebp: ToWebp = (avatar: Avatar, options: Options = {}) => { + return toFormat(avatar, 'webp', options); }; -export const toAvif: ToAvif = (avatar: Avatar) => { - return toFormat(avatar, 'avif'); +export const toAvif: ToAvif = (avatar: Avatar, options: Options = {}) => { + return toFormat(avatar, 'avif', options); }; function toFormat( avatar: Avatar, format: 'png' | 'jpeg' | 'webp' | 'avif', + options: Options = {}, ): Result { + if (options.includeExif) { + console.warn( + 'The `exif` option is not supported in the browser version of `@dicebear/converter`. \n' + + 'Please use the node version of `@dicebear/converter` to generate images with exif data.', + ); + } + const svg = typeof avatar === 'string' ? avatar : avatar.toString(); return { - toDataUri: () => toDataUri(svg, format), - toArrayBuffer: () => toArrayBuffer(svg, format), + toDataUri: () => toDataUri(svg, format, options), + toArrayBuffer: () => toArrayBuffer(svg, format, options), }; } async function toDataUri( svg: string, format: 'svg' | 'png' | 'jpeg' | 'webp' | 'avif', - exif?: Exif, + options: Options, ): Promise<string> { if ('svg' === format) { return `data:${getMimeType(format)};utf8,${encodeURIComponent(svg)}`; } - const canvas = await toCanvas(svg, format, exif); + const canvas = await toCanvas(svg, format, options); return canvas.toDataURL(getMimeType(format)); } async function toArrayBuffer( rawSvg: string, format: 'png' | 'jpeg' | 'webp' | 'avif', - exif?: Exif, + options: Options, ): Promise<ArrayBufferLike> { - const canvas = await toCanvas(rawSvg, format, exif); + const canvas = await toCanvas(rawSvg, format, options); return await new Promise<ArrayBufferLike>((resolve, reject) => { canvas.toBlob((blob) => { @@ -71,16 +80,9 @@ async function toArrayBuffer( async function toCanvas( rawSvg: string, format: 'png' | 'jpeg' | 'webp' | 'avif', - exif?: Exif, + options: Options, ): Promise<HTMLCanvasElement> { - if (exif) { - console.warn( - 'The `exif` option is not supported in the browser version of `@dicebear/converter`. \n' + - 'Please use the node version of `@dicebear/converter` to generate images with exif data.', - ); - } - - const { svg, size } = ensureSize(rawSvg); + const { svg, size } = ensureSize(rawSvg, options.size); const canvas = document.createElement('canvas'); canvas.width = size; @@ -101,7 +103,7 @@ async function toCanvas( img.width = size; img.height = size; - img.setAttribute('src', await toDataUri(svg, 'svg')); + img.setAttribute('src', await toDataUri(svg, 'svg', options)); return new Promise((resolve, reject) => { img.onload = () => {
packages/@dicebear/converter/src/node/core.ts+1 −1 modified@@ -80,7 +80,7 @@ async function toBuffer( ): Promise<Buffer> { const hasFonts = Array.isArray(options.fonts); - const { svg } = ensureSize(rawSvg); + const { svg } = ensureSize(rawSvg, options.size); let buffer = ( await renderAsync(svg, {
packages/@dicebear/converter/src/types.ts+1 −0 modified@@ -18,6 +18,7 @@ export interface Exif { } export interface Options { + size?: number; fonts?: string[]; includeExif?: boolean; }
packages/@dicebear/converter/src/utils/svg.ts+12 −7 modified@@ -1,16 +1,21 @@ import { XMLParser } from 'fast-xml-parser'; import { Metadata } from '../types'; -export function ensureSize(svg: string, defaultSize: number = 512) { - let size = defaultSize; +const MAX_SIZE = 2048; +const DEFAULT_SIZE = 512; - svg = svg.replace(/<svg([^>]*)/, (match, g1) => { - const found = g1.match(/width="([^"]+)"/); +function sanitizeSize(size: number): number { + if (!Number.isFinite(size) || size <= 0) { + return DEFAULT_SIZE; + } - if (found) { - size = parseInt(found[1]); - } + return Math.floor(Math.min(size, MAX_SIZE)); +} +export function ensureSize(svg: string, size: number = DEFAULT_SIZE) { + size = sanitizeSize(size); + + svg = svg.replace(/<svg([^>]*)/, (match, g1) => { if (g1.match(/width="([^"]+)"/)) { g1 = g1.replace(/width="([^"]+)"/, `width="${size}"`); } else {
packages/@dicebear/converter/tests/png.test.js+22 −0 modified@@ -4,6 +4,7 @@ import * as path from 'path'; import { fileURLToPath } from 'url'; import { test } from 'node:test'; import assert from 'node:assert/strict'; +import sharp from 'sharp'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const avatar = fs.readFileSync(path.resolve(__dirname, 'fixtures/avatar.svg'), { @@ -17,3 +18,24 @@ test(`Convert to png buffer`, async () => { test(`Convert to png data uri`, async () => { assert.doesNotThrow(() => toPng(avatar).toDataUri()); }); + +test(`PNG output respects size option`, async () => { + const buffer = await toPng(avatar, { size: 128 }).toArrayBuffer(); + const metadata = await sharp(Buffer.from(buffer)).metadata(); + assert.equal(metadata.width, 128); + assert.equal(metadata.height, 128); +}); + +test(`PNG output defaults to 512`, async () => { + const buffer = await toPng(avatar).toArrayBuffer(); + const metadata = await sharp(Buffer.from(buffer)).metadata(); + assert.equal(metadata.width, 512); + assert.equal(metadata.height, 512); +}); + +test(`PNG output clamps oversized value to 2048`, async () => { + const buffer = await toPng(avatar, { size: 99999 }).toArrayBuffer(); + const metadata = await sharp(Buffer.from(buffer)).metadata(); + assert.equal(metadata.width, 2048); + assert.equal(metadata.height, 2048); +});
packages/@dicebear/converter/tests/utils/svg.test.js+55 −3 modified@@ -17,14 +17,14 @@ test(`"ensureSize" without width and height`, async () => { test(`"ensureSize" with width and height`, async () => { equal( ensureSize(`<svg foo width="20" bar height="20"></svg>`, 100).svg, - `<svg foo width="20" bar height="20"></svg>`, + `<svg foo width="100" bar height="100"></svg>`, ); }); test(`"ensureSize" with width only`, async () => { equal( ensureSize(`<svg foo width="20" bar></svg>`, 100).svg, - `<svg foo width="20" bar height="20"></svg>`, + `<svg foo width="100" bar height="100"></svg>`, ); }); @@ -35,11 +35,63 @@ test(`"ensureSize" with height only`, async () => { ); }); +test(`"ensureSize" returns correct size`, async () => { + equal(ensureSize(`<svg></svg>`, 256).size, 256); +}); + +test(`"ensureSize" defaults to 512`, async () => { + const result = ensureSize(`<svg></svg>`); + equal(result.size, 512); + equal(result.svg, `<svg width="512" height="512"></svg>`); +}); + +test(`"ensureSize" overwrites huge SVG dimensions`, async () => { + equal( + ensureSize(`<svg width="999999999" height="999999999"></svg>`, 128).svg, + `<svg width="128" height="128"></svg>`, + ); +}); + +test(`"ensureSize" overwrites non-numeric SVG dimensions`, async () => { + equal( + ensureSize(`<svg width="100%" height="auto"></svg>`, 64).svg, + `<svg width="64" height="64"></svg>`, + ); +}); + +test(`"ensureSize" clamps to max 2048`, async () => { + const result = ensureSize(`<svg></svg>`, 10000); + equal(result.size, 2048); + equal(result.svg, `<svg width="2048" height="2048"></svg>`); +}); + +test(`"ensureSize" floors fractional size`, async () => { + const result = ensureSize(`<svg></svg>`, 99.9); + equal(result.size, 99); + equal(result.svg, `<svg width="99" height="99"></svg>`); +}); + +test(`"ensureSize" falls back to 512 for NaN`, async () => { + equal(ensureSize(`<svg></svg>`, NaN).size, 512); +}); + +test(`"ensureSize" falls back to 512 for negative`, async () => { + equal(ensureSize(`<svg></svg>`, -100).size, 512); +}); + +test(`"ensureSize" falls back to 512 for zero`, async () => { + equal(ensureSize(`<svg></svg>`, 0).size, 512); +}); + +test(`"ensureSize" falls back to 512 for Infinity`, async () => { + equal(ensureSize(`<svg></svg>`, Infinity).size, 512); +}); + test(`Metadata parsing`, async () => { const avatar = await fs.readFile(path.resolve(__dirname, '../fixtures/avatar.svg'), { encoding: 'utf8', }); - + equal( getMetadata(avatar), {
packages/dicebear/src/utils/addStyleCommand.ts+4 −1 modified@@ -84,7 +84,7 @@ export function addStyleCommand( case 'png': await writeFile( fileName, - await toPng(avatar.toString(), { includeExif }).toArrayBuffer(), + await toPng(avatar.toString(), { includeExif, size: validated.size as number }).toArrayBuffer(), ); break; @@ -94,6 +94,7 @@ export function addStyleCommand( fileName, await toJpeg(avatar.toString(), { includeExif, + size: validated.size as number, }).toArrayBuffer(), ); break; @@ -103,6 +104,7 @@ export function addStyleCommand( fileName, await toWebp(avatar.toString(), { includeExif, + size: validated.size as number, }).toArrayBuffer(), ); break; @@ -112,6 +114,7 @@ export function addStyleCommand( fileName, await toAvif(avatar.toString(), { includeExif, + size: validated.size as number, }).toArrayBuffer(), ); break;
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-v3r3-4qgc-vw66ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-29112ghsaADVISORY
- github.com/dicebear/dicebear/commit/42a59eac46a3c68598859e608ec45e578b27614aghsax_refsource_MISCWEB
- github.com/dicebear/dicebear/releases/tag/v9.4.0ghsax_refsource_MISCWEB
- github.com/dicebear/dicebear/security/advisories/GHSA-v3r3-4qgc-vw66ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.