VYPR
Medium severity5.3NVD Advisory· Published Apr 2, 2026· Updated Apr 29, 2026

CVE-2026-5323

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.

PackageAffected versionsPatched versions
a11y-mcpnpm
< 1.0.51.0.5

Affected products

1

Patches

1
e3e11c9e8482

fix: add SSRF protection with URL validation (CVE pending)

https://github.com/priyankark/a11y-mcpPriyankar KumarMar 22, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.