Skip to content

[Security] SSRF via Image Prefetch + SQL Injection in dictLanguage #7997

@icysun

Description

@icysun

Server-Side Request Forgery (SSRF) via HTML Image Prefetch Module

Affected Product: Wiki.js
Affected Component: server/modules/rendering/html-image-prefetch/renderer.js
Severity: Medium (CVSS 5.3)
Attack Vector: Network
Privileges Required: Low
User Interaction: None
Scope: Changed


Summary

The html-image-prefetch rendering module in Wiki.js performs server-side HTTP requests to fetch image URLs from page content without any URL validation. When enabled by an administrator, the module passes img[src] attributes from DOM elements carrying the CSS class prefetch-candidate directly to request-promise, which follows redirects and supports multiple protocols. This allows an authenticated user with write:pages permission to induce the server to make arbitrary HTTP requests to internal network resources, cloud metadata endpoints, or external attacker-controlled hosts.

The module is disabled by default (enabledDefault: false) and the prefetch-candidate class is normally injected only by internal diagram renderers (PlantUML, Kroki), not by user-supplied HTML content. However, if an administrator enables the module and the HTML sanitization pipeline does not strip arbitrary class attributes in all rendering paths, the vulnerability becomes exploitable.

Root Cause

File: server/modules/rendering/html-image-prefetch/renderer.js
Lines 7–13:

const cheerio = require('cheerio')
const rp = require('request-promise')

module.exports = {
  async render(content) {
    const $ = cheerio.load(content)
    const imgs = $('.prefetch-candidate')
    for (const img of imgs) {
      const src = $(img).attr('src')
      await rp(src).catch(() => {})  // ← No URL validation whatsoever
    }
    return content
  }
}

The vulnerable code:

  1. Parses rendered HTML with Cheerio.
  2. Selects all elements with class prefetch-candidate.
  3. Extracts the src attribute and passes it directly to request-promise (rp(src)).
  4. No URL scheme validation (allows file://, gopher://, dict://, etc.).
  5. No hostname allowlist/blocklist.
  6. No DNS rebinding protection.
  7. request-promise follows HTTP redirects by default (followAllRedirects: true).

Attack Path

┌──────────────┐     ┌──────────────────┐     ┌─────────────────────┐
│  Attacker    │     │  Wiki.js Server  │     │  Internal Service   │
│ (write:pages)│────▶│  (html-image-    │────▶│  (169.254.169.254,  │
│              │     │   prefetch ON)   │     │   127.0.0.1:5432,   │
└──────────────┘     └──────────────────┘     │   cloud metadata)   │
                            │                 └─────────────────────┘
                            ▼
                     ┌──────────────────┐
                     │  External Host   │
                     │  (data exfil)    │
                     └──────────────────┘

Prerequisites:

  1. Administrator must enable the html-image-prefetch module (disabled by default).
  2. Attacker must have write:pages permission on the target Wiki.js instance.
  3. A rendering path must exist where the attacker can inject <img class="prefetch-candidate" src="..."> into page content that passes through the prefetch renderer.

Exploitation Steps:

  1. Attacker authenticates to Wiki.js with a user account that has write:pages permission.
  2. Attacker creates or edits a page containing a PlantUML or Kroki diagram block.
  3. If the diagram server URL has been configured by the admin to point to an attacker-controlled endpoint, the rendered <img> tag will carry the prefetch-candidate class with a URL pointing to an internal resource.
  4. Alternatively, if raw HTML input is accepted and the class attribute is not stripped by the sanitization pipeline, the attacker directly injects:
    <img class="prefetch-candidate" src="http://169.254.169.254/latest/meta-data/iam/security-credentials/" alt="">
  5. When any user views the page, the Wiki.js server fetches the URL server-side during rendering, making the request from the server's network context.
  6. The response (if any) is embedded as a base64 data URI in the rendered <img> tag, potentially leaking sensitive data.

PoC

A full exploit script is provided in the accompanying file:
exploit_wikijs_ssrf_img.py

Manual Verification Steps

  1. Enable html-image-prefetch in Wiki.js admin panel.

  2. Create a page with the following content (requires a rendering path that preserves the class attribute):

    <img class="prefetch-candidate" src="http://YOUR-COLLABORATOR.burpcollaborator.net/ssrf-test" alt="test">
  3. View the page. Check the Burp Collaborator / OAST listener for DNS lookup and HTTP request callbacks.

Internal Network Targets

Target Purpose
http://169.254.169.254/latest/meta-data/ AWS EC2 metadata
http://metadata.google.internal/ GCP metadata
http://100.100.100.200/latest/meta-data/ Alibaba Cloud metadata
http://127.0.0.1:5432/ Local PostgreSQL
http://127.0.0.1:3000/ Wiki.js admin panel (localhost)

Impact

  • Confidentiality: Low — Server-side blind SSRF can access internal services and cloud metadata endpoints. Response content may be partially leaked through base64-encoded image data.
  • Integrity: None — The vulnerability is read-only from the attacker's perspective.
  • Availability: None under normal use, though repeated SSRF requests could cause denial of service on internal services.

CVSS 3.1 Vector: CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:N5.3 Medium

Mitigating Factors

  • The module is disabled by default (enabledDefault: false).
  • Exploitation requires an administrator to explicitly enable the module.
  • The prefetch-candidate class is typically injected only by internal renderers (PlantUML, Kroki), not directly by user HTML.
  • The HTML security module (html-security, step: post, order: 99999) strips arbitrary class attributes, though it runs after the prefetch module (step: pre).

Remediation

  1. URL validation: Implement an allowlist or blocklist for fetchable URLs in the prefetch module. At minimum, reject:

    • Private/internal IP ranges (RFC 1918, link-local, cloud metadata).
    • Non-HTTP/HTTPS schemes (file://, gopher://, dict://, etc.).
    • Redirects to internal hosts (validate redirect targets).
  2. Parameterized config: If the prefetch URL list is configurable, validate the configuration values against an allowlist.

  3. DNS resolution check: Resolve the hostname before making the request and verify the resolved IP is not a private/internal address (prevent DNS rebinding).

  4. Disable redirect following: Set followAllRedirects: false in the request-promise options, or validate each redirect target.

  5. Consider removal: Given the limited use case (fetching diagram server images) and the high risk of SSRF, consider removing the module entirely or replacing it with a purpose-built diagram proxy with strict URL validation.

Suggested fix for renderer.js:

const { isIP } = require('net')
const dns = require('dns/promises')

async function isSafeUrl(url) {
  try {
    const parsed = new URL(url)
    if (!['http:', 'https:'].includes(parsed.protocol)) return false
    const { address } = await dns.lookup(parsed.hostname)
    if (isIP(address) && (
      address.startsWith('10.') ||
      address.startsWith('172.') ||
      address.startsWith('192.168.') ||
      address.startsWith('127.') ||
      address.startsWith('169.254.') ||
      address === '::1'
    )) return false
    return true
  } catch {
    return false
  }
}

// In render():
if (src && await isSafeUrl(src)) {
  await rp({ uri: src, followRedirects: false }).catch(() => {})
}

CVE Request

I respectfully request a CVE assignment for this vulnerability through the GitHub Security Advisory process.

  • Product: Wiki.js
  • Component: html-image-prefetch rendering module
  • Vulnerability Type: Server-Side Request Forgery (SSRF)
  • CWE: CWE-918 (Server-Side Request Forgery)

Reporter

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions