CVE-2026-5323
Description
A vulnerability was found in priyankark a11y-mcp up to 1.0.5. This vulnerability affects the function A11yServer of the file src/index.js. The manipulation results in server-side request forgery. The attack must be initiated from a local position. The exploit has been made public and could be used. This product operates on a rolling release basis, ensuring continuous delivery. Consequently, there are no version details for either affected or updated releases. Upgrading to version 1.0.6 is able to resolve this issue. The patch is identified as e3e11c9e8482bd06b82fd9fced67be4856f0dffc. It is recommended to upgrade the affected component. The vendor acknowledged the issue but provides additional context for the CVSS rating: "a11y-mcp is a local stdio MCP server - it has no HTTP endpoint and is not network-accessible. The caller is always the local user or an LLM acting on their behalf with user approval."
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
a11y-mcpnpm | < 1.0.5 | 1.0.5 |
Affected products
1Patches
1e3e11c9e8482fix: add SSRF protection with URL validation (CVE pending)
2 files changed · +91 −11
package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "a11y-mcp", - "version": "1.0.4", + "version": "1.0.5", "main": "src/index.js", "type": "module", "bin": {
src/index.js+90 −10 modified@@ -7,9 +7,83 @@ import { ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; +import { lookup } from 'node:dns/promises'; import puppeteer from 'puppeteer'; import { AxePuppeteer } from '@axe-core/puppeteer'; +/** + * Validate that a URL is safe to navigate to (SSRF protection). + * Only allows http/https schemes and blocks requests to internal networks. + */ +async function validateUrl(urlString) { + let parsed; + try { + parsed = new URL(urlString); + } catch { + throw new Error('Invalid URL format'); + } + + // Only allow http and https schemes + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error(`Disallowed URL scheme: ${parsed.protocol}`); + } + + const hostname = parsed.hostname; + + // Block obvious localhost/loopback hostnames + const blockedHostnames = ['localhost', '127.0.0.1', '::1', '0.0.0.0', '[::1]']; + if (blockedHostnames.includes(hostname.toLowerCase())) { + throw new Error('URLs pointing to loopback addresses are not allowed'); + } + + // Resolve the hostname and check the resulting IP + let address; + try { + const result = await lookup(hostname); + address = result.address; + } catch { + throw new Error(`Unable to resolve hostname: ${hostname}`); + } + + if (isPrivateIP(address)) { + throw new Error('URLs pointing to private or internal network addresses are not allowed'); + } + + return parsed; +} + +/** + * Check if an IP address belongs to a private, loopback, or link-local range. + */ +function isPrivateIP(ip) { + // IPv4 checks + const parts = ip.split('.').map(Number); + if (parts.length === 4 && parts.every(p => p >= 0 && p <= 255)) { + // 127.0.0.0/8 — loopback + if (parts[0] === 127) return true; + // 10.0.0.0/8 — private + if (parts[0] === 10) return true; + // 172.16.0.0/12 — private + if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; + // 192.168.0.0/16 — private + if (parts[0] === 192 && parts[1] === 168) return true; + // 169.254.0.0/16 — link-local / cloud metadata + if (parts[0] === 169 && parts[1] === 254) return true; + // 0.0.0.0/8 + if (parts[0] === 0) return true; + } + + // IPv6 loopback + if (ip === '::1' || ip === '0:0:0:0:0:0:0:1') return true; + // IPv6 link-local + if (ip.toLowerCase().startsWith('fe80:')) return true; + // IPv6 unique local (fc00::/7) + const first2 = ip.toLowerCase().slice(0, 2); + if (first2 === 'fc' || first2 === 'fd') return true; + + return false; +} + class A11yServer { constructor() { this.server = new Server( @@ -104,18 +178,21 @@ class A11yServer { } try { + // Validate URL to prevent SSRF + const validatedUrl = await validateUrl(args.url); + const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'], }); const page = await browser.newPage(); - + // Set a reasonable viewport await page.setViewport({ width: 1280, height: 800 }); - - // Navigate to the page - await page.goto(args.url, { waitUntil: 'networkidle2', timeout: 30000 }); - + + // Navigate to the page using the validated URL + await page.goto(validatedUrl.href, { waitUntil: 'networkidle2', timeout: 30000 }); + // Run axe on the page const axeOptions = {}; if (args.tags && args.tags.length > 0) { @@ -192,18 +269,21 @@ class A11yServer { } try { + // Validate URL to prevent SSRF + const validatedUrl = await validateUrl(args.url); + const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'], }); const page = await browser.newPage(); - + // Set a reasonable viewport await page.setViewport({ width: 1280, height: 800 }); - - // Navigate to the page - await page.goto(args.url, { waitUntil: 'networkidle2', timeout: 30000 }); - + + // Navigate to the page using the validated URL + await page.goto(validatedUrl.href, { waitUntil: 'networkidle2', timeout: 30000 }); + // Run axe on the page const results = await new AxePuppeteer(page).analyze();
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-prmx-7v35-7q82ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-5323ghsaADVISORY
- github.com/priyankark/a11y-mcp/commit/e3e11c9e8482bd06b82fd9fced67be4856f0dffcnvdWEB
- github.com/wing3e/public_exp/issues/17nvdWEB
- vuldb.com/submit/780752nvdWEB
- vuldb.com/vuln/354655nvdWEB
- vuldb.com/vuln/354655/ctinvdWEB
News mentions
0No linked articles in our index yet.