VYPR
High severityNVD Advisory· Published Mar 18, 2026· Updated Mar 18, 2026

@dicebear/converter vulnerable to ncontrolled memory allocation via crafted SVG dimensions

CVE-2026-29112

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.

PackageAffected versionsPatched versions
@dicebear/converternpm
< 9.4.09.4.0

Affected products

2
  • Dicebear/Dicebearllm-fuzzy2 versions
    <9.4.0+ 1 more
    • (no CPE)range: <9.4.0
    • (no CPE)range: < 9.4.0

Patches

1
42a59eac46a3

fix: prevent DoS via crafted SVG dimensions in @dicebear/converter

https://github.com/dicebear/dicebearFlorian KörnerMar 3, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.