VYPR
Medium severity6.5GHSA Advisory· Published Jun 16, 2026· Updated Jun 16, 2026

Deno: Node TCPWrap numeric hostname aliases bypass --deny-net resolved-IP deny checks

CVE-2026-49411

Description

Deno's --deny-net permission is bypassed in Node.js compatibility paths when numeric IP aliases are used, allowing unintended loopback access.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Deno's `--deny-net` permission is bypassed in Node.js compatibility paths when numeric IP aliases are used, allowing unintended loopback access.

Vulnerability

In Deno versions prior to the fix (e.g., 2.7.14), the Node.js compatibility TCP path (via node:net.connect and node:http.request with options-form host) fails to re-check --deny-net permissions after DNS resolution. The permission check is performed only against the literal hostname string supplied by the caller, not the resolved IP address. Consequently, a numeric alias of an IP address (e.g., decimal integer 2130706433 or hex 0x7f000001, both resolving to 127.0.0.1) can bypass a --deny-net=127.0.0.1 or --deny-net=127.0.0.0/8 rule. The native Deno.connect(), fetch(), and URL-string forms of node:http.request are not affected because they either re-check after resolution or normalize the hostname during URL parsing [1][2].

Exploitation

An attacker requires the ability to execute JavaScript code within a Deno process that has --allow-net granted but is also subject to --deny-net rules blocking specific IP addresses (e.g., loopback). The attacker can call node:net.connect({ host: "2130706433", port: PORT }) or node:http.request({ hostname: "2130706433", port: PORT, path: "/" }) to reach the denied destination. The numeric alias bypasses the pre-resolution permission check because the check matches against the alias string rather than the resolved IP. No additional privileges or user interaction are required beyond the initial code execution [1][2].

Impact

An attacker can make outbound TCP connections or HTTP requests to IP addresses that the administrator intended to block with --deny-net. In particular, loopback addresses (127.0.0.0/8) can be reached, which may allow access to local services that should be protected. This bypass results in a failure of the network permission model, enabling information disclosure or further compromise of local services depending on what is listening on the denied IP [1][2].

Mitigation

A fix was released in the GitHub Advisory (GHSA-v8fw-85r8-5m23) and included in Deno versions after 2.7.14. Users should update to the latest stable version of Deno that includes the patch. There is no known workaround apart from avoiding the use of node:net or node:http options-form APIs with untrusted hostnames. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog as of publication [1][2].

AI Insight generated on Jun 16, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
241d11f9a39f

fix(permissions): check deny rules against resolved IPs to prevent numeric hostname bypass (#33203)

https://github.com/denoland/denoDivy SrivastavaApr 9, 2026via body-scan-shorthand
11 files changed · +269 0
  • ext/net/ops.rs+27 0 modified
    @@ -267,6 +267,17 @@ pub async fn op_net_send_udp(
         .next()
         .ok_or(NetError::NoResolvedAddress)?;
     
    +  {
    +    state
    +      .borrow_mut()
    +      .borrow_mut::<PermissionsContainer>()
    +      .check_net_resolved(
    +        &addr.ip(),
    +        addr.port(),
    +        "Deno.DatagramConn.send()",
    +      )?;
    +  }
    +
       let resource = state
         .borrow_mut()
         .resource_table
    @@ -508,6 +519,16 @@ pub async fn op_net_connect_tcp_inner(
         .next()
         .ok_or_else(|| NetError::NoResolvedAddress)?;
     
    +  // Post-resolution deny check: verify the resolved IP is not denied.
    +  // This prevents bypassing IP-literal deny rules via numeric hostname
    +  // aliases (e.g. 2130706433 → 127.0.0.1).
    +  {
    +    state
    +      .borrow_mut()
    +      .borrow_mut::<PermissionsContainer>()
    +      .check_net_resolved(&addr.ip(), addr.port(), "Deno.connect()")?;
    +  }
    +
       let cancel_handle = resource_abort_id.and_then(|rid| {
         state
           .borrow_mut()
    @@ -576,6 +597,9 @@ pub fn op_net_listen_tcp(
       let addr = resolve_addr_sync(&addr.hostname, addr.port)?
         .next()
         .ok_or_else(|| NetError::NoResolvedAddress)?;
    +  state
    +    .borrow_mut::<PermissionsContainer>()
    +    .check_net_resolved(&addr.ip(), addr.port(), "Deno.listen()")?;
     
       let listener = if load_balanced {
         TcpListener::bind_load_balanced(addr, tcp_backlog)
    @@ -601,6 +625,9 @@ fn net_listen_udp(
       let addr = resolve_addr_sync(&addr.hostname, addr.port)?
         .next()
         .ok_or_else(|| NetError::NoResolvedAddress)?;
    +  state
    +    .borrow_mut::<PermissionsContainer>()
    +    .check_net_resolved(&addr.ip(), addr.port(), "Deno.listenDatagram()")?;
     
       let domain = if addr.is_ipv4() {
         Domain::IPV4
    
  • ext/net/ops_tls.rs+15 0 modified
    @@ -462,6 +462,14 @@ pub async fn op_net_connect_tls(
         .await?
         .next()
         .ok_or_else(|| NetError::NoResolvedAddress)?;
    +  state
    +    .borrow_mut()
    +    .borrow_mut::<PermissionsContainer>()
    +    .check_net_resolved(
    +      &connect_addr.ip(),
    +      connect_addr.port(),
    +      "Deno.connectTls()",
    +    )?;
       let tcp_stream = TcpStream::connect(connect_addr).await?;
       let local_addr = tcp_stream.local_addr()?;
       let remote_addr = tcp_stream.peer_addr()?;
    @@ -528,6 +536,13 @@ pub fn op_net_listen_tls(
       let bind_addr = resolve_addr_sync(&addr.hostname, addr.port)?
         .next()
         .ok_or(NetError::NoResolvedAddress)?;
    +  state
    +    .borrow_mut::<PermissionsContainer>()
    +    .check_net_resolved(
    +      &bind_addr.ip(),
    +      bind_addr.port(),
    +      "Deno.listenTls()",
    +    )?;
     
       let tcp_listener = if args.load_balanced {
         TcpListener::bind_load_balanced(bind_addr, args.tcp_backlog)
    
  • ext/net/quic.rs+8 0 modified
    @@ -556,6 +556,14 @@ pub(crate) fn op_quic_endpoint_connect(
       let sock_addr = resolve_addr_sync(&args.addr.hostname, args.addr.port)?
         .next()
         .ok_or_else(|| QuicError::UnableToResolve)?;
    +  state
    +    .borrow_mut()
    +    .borrow_mut::<PermissionsContainer>()
    +    .check_net_resolved(
    +      &sock_addr.ip(),
    +      sock_addr.port(),
    +      "Deno.connectQuic()",
    +    )?;
     
       let root_cert_store = state
         .borrow()
    
  • ext/node/ops/udp.rs+9 0 modified
    @@ -79,6 +79,9 @@ pub fn op_node_udp_bind(
       let addr = deno_net::resolve_addr::resolve_addr_sync(hostname, port)?
         .next()
         .ok_or(NodeUdpError::NoResolvedAddress)?;
    +  state
    +    .borrow_mut::<PermissionsContainer>()
    +    .check_net_resolved(&addr.ip(), addr.port(), "dgram.createSocket()")?;
     
       let domain = if addr.is_ipv4() {
         Domain::IPV4
    @@ -567,6 +570,12 @@ pub async fn op_node_udp_send(
         deno_net::resolve_addr::resolve_addr_sync(&hostname, port)?
           .next()
           .ok_or(NodeUdpError::NoResolvedAddress)?;
    +  {
    +    state
    +      .borrow_mut()
    +      .borrow_mut::<PermissionsContainer>()
    +      .check_net_resolved(&addr.ip(), addr.port(), "socket.send()")?;
    +  }
     
       let cancel = RcRef::map(&resource, |r| &r.cancel);
       let nwritten = resource
    
  • runtime/permissions/lib.rs+133 0 modified
    @@ -3163,6 +3163,46 @@ impl UnaryPermission<NetDescriptor> {
         );
         self.check_desc(None, false, None)
       }
    +
    +  /// Check if a resolved IP address is explicitly denied.
    +  ///
    +  /// This is used after DNS resolution to ensure that deny rules written as
    +  /// IP literals (e.g. `--deny-net=127.0.0.1`) also block connections made
    +  /// via hostname aliases that resolve to the denied IP (e.g. numeric forms
    +  /// like `2130706433` which the OS resolver maps to `127.0.0.1`).
    +  ///
    +  /// Only checks deny rules — the allow check has already been performed
    +  /// against the original hostname.
    +  pub fn check_resolved_ip_deny(
    +    &mut self,
    +    desc: &NetDescriptor,
    +    api_name: Option<&str>,
    +  ) -> Result<(), PermissionDeniedError> {
    +    let info = format_display_name(desc.display_name()).into_owned();
    +    let denied = || {
    +      PermissionState::permission_denied_error(
    +        NetDescriptor::flag_name(),
    +        Some(info.as_str()),
    +        PermissionState::Denied,
    +      )
    +    };
    +    if self.flag_denied_global {
    +      return Err(denied());
    +    }
    +    for item in self.descriptors.iter() {
    +      match item {
    +        UnaryPermissionDesc::FlagDenied(v)
    +        | UnaryPermissionDesc::FlagIgnored(v) => {
    +          if desc.matches_deny(v) {
    +            return Err(denied());
    +          }
    +        }
    +        _ => {}
    +      }
    +    }
    +    let _ = api_name;
    +    Ok(())
    +  }
     }
     
     impl UnaryPermission<ImportDescriptor> {
    @@ -4424,6 +4464,22 @@ impl PermissionsContainer {
         Ok(())
       }
     
    +  /// After resolving a hostname to an IP address, check that the resolved
    +  /// IP is not in the deny list. This prevents bypassing IP-literal deny
    +  /// rules via numeric hostname aliases or attacker-controlled DNS.
    +  #[inline(always)]
    +  pub fn check_net_resolved(
    +    &mut self,
    +    resolved_ip: &std::net::IpAddr,
    +    port: u16,
    +    api_name: &str,
    +  ) -> Result<(), PermissionCheckError> {
    +    let mut inner = self.inner.lock();
    +    let desc = NetDescriptor(Host::Ip(*resolved_ip), Some(port.into()));
    +    inner.net.check_resolved_ip_deny(&desc, Some(api_name))?;
    +    Ok(())
    +  }
    +
       #[inline(always)]
       pub fn check_net_vsock(
         &mut self,
    @@ -5684,6 +5740,83 @@ mod tests {
         }
       }
     
    +  #[test]
    +  fn test_check_net_deny_resolved_ip() {
    +    // Regression test: deny rules written as IP literals must also block
    +    // connections after DNS resolution, preventing bypasses via numeric
    +    // hostname aliases (e.g. 2130706433 → 127.0.0.1).
    +    set_prompter(Box::new(TestPrompter));
    +    let parser = TestPermissionDescriptorParser;
    +    let mut perms = Permissions::from_options(
    +      &parser,
    +      &PermissionsOptions {
    +        allow_net: Some(svec![]),
    +        deny_net: Some(svec!["127.0.0.1"]),
    +        ..Default::default()
    +      },
    +    )
    +    .unwrap();
    +
    +    // The resolved IP 127.0.0.1 should be denied regardless of original
    +    // hostname.
    +    let denied_ip = std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
    +    let desc = NetDescriptor(Host::Ip(denied_ip), Some(12345));
    +    assert!(
    +      perms.net.check_resolved_ip_deny(&desc, None).is_err(),
    +      "resolved 127.0.0.1 should be denied"
    +    );
    +
    +    // A different IP should not be denied.
    +    let allowed_ip = std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
    +    let desc = NetDescriptor(Host::Ip(allowed_ip), Some(12345));
    +    assert!(
    +      perms.net.check_resolved_ip_deny(&desc, None).is_ok(),
    +      "resolved 192.168.1.1 should not be denied"
    +    );
    +  }
    +
    +  #[test]
    +  fn test_check_net_deny_resolved_ip_subnet() {
    +    // Regression test: subnet-based deny rules (e.g. --deny-net=127.0.0.0/8)
    +    // must also block resolved IPs that fall within the subnet, preventing
    +    // bypasses via numeric hostname aliases (e.g. 2130706433 → 127.0.0.1).
    +    set_prompter(Box::new(TestPrompter));
    +    let parser = TestPermissionDescriptorParser;
    +    let mut perms = Permissions::from_options(
    +      &parser,
    +      &PermissionsOptions {
    +        allow_net: Some(svec![]),
    +        deny_net: Some(svec!["127.0.0.0/8"]),
    +        ..Default::default()
    +      },
    +    )
    +    .unwrap();
    +
    +    // 127.0.0.1 falls within the 127.0.0.0/8 subnet — should be denied.
    +    let denied_ip = std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
    +    let desc = NetDescriptor(Host::Ip(denied_ip), Some(8000));
    +    assert!(
    +      perms.net.check_resolved_ip_deny(&desc, None).is_err(),
    +      "resolved 127.0.0.1 should be denied by 127.0.0.0/8 subnet rule"
    +    );
    +
    +    // 127.1.2.3 also falls within 127.0.0.0/8 — should be denied.
    +    let denied_ip2 = std::net::IpAddr::V4(Ipv4Addr::new(127, 1, 2, 3));
    +    let desc = NetDescriptor(Host::Ip(denied_ip2), Some(9000));
    +    assert!(
    +      perms.net.check_resolved_ip_deny(&desc, None).is_err(),
    +      "resolved 127.1.2.3 should be denied by 127.0.0.0/8 subnet rule"
    +    );
    +
    +    // 192.168.1.1 is outside the subnet — should not be denied.
    +    let allowed_ip = std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
    +    let desc = NetDescriptor(Host::Ip(allowed_ip), Some(8000));
    +    assert!(
    +      perms.net.check_resolved_ip_deny(&desc, None).is_ok(),
    +      "resolved 192.168.1.1 should not be denied by 127.0.0.0/8 subnet rule"
    +    );
    +  }
    +
       #[test]
       fn test_check_net_url() {
         let parser = TestPermissionDescriptorParser;
    
  • tests/specs/permission/deny_net_numeric_hostname/main.out+3 0 added
    @@ -0,0 +1,3 @@
    +PASS: direct 127.0.0.1 denied
    +PASS: numeric 2130706433 denied
    +PASS: hex 0x7f000001 denied
    
  • tests/specs/permission/deny_net_numeric_hostname/main.ts+27 0 added
    @@ -0,0 +1,27 @@
    +// Regression test: --deny-net=127.0.0.1 must block connections via numeric
    +// hostname aliases that the OS resolver maps to the denied IP.
    +// e.g. 2130706433 is the decimal representation of 127.0.0.1.
    +
    +// Try connecting to 127.0.0.1 directly — should be denied.
    +try {
    +  await Deno.connect({ hostname: "127.0.0.1", port: 12345 });
    +  console.log("FAIL: direct 127.0.0.1 was not denied");
    +} catch {
    +  console.log("PASS: direct 127.0.0.1 denied");
    +}
    +
    +// Try connecting via decimal numeric hostname 2130706433 — should also be denied.
    +try {
    +  await Deno.connect({ hostname: "2130706433", port: 12345 });
    +  console.log("FAIL: numeric 2130706433 was not denied");
    +} catch {
    +  console.log("PASS: numeric 2130706433 denied");
    +}
    +
    +// Try connecting via 0x7f000001 (hex form) — should also be denied.
    +try {
    +  await Deno.connect({ hostname: "0x7f000001", port: 12345 });
    +  console.log("FAIL: hex 0x7f000001 was not denied");
    +} catch {
    +  console.log("PASS: hex 0x7f000001 denied");
    +}
    
  • tests/specs/permission/deny_net_numeric_hostname_subnet/main.out+3 0 added
    @@ -0,0 +1,3 @@
    +PASS: direct 127.0.0.1 denied
    +PASS: numeric 2130706433 denied
    +PASS: hex 0x7f000001 denied
    
  • tests/specs/permission/deny_net_numeric_hostname_subnet/main.ts+28 0 added
    @@ -0,0 +1,28 @@
    +// Regression test: --deny-net=127.0.0.0/8 (subnet deny) must also block
    +// connections via numeric hostname aliases that resolve to IPs within
    +// the denied subnet.
    +// e.g. 2130706433 is the decimal representation of 127.0.0.1.
    +
    +// Try connecting to 127.0.0.1 directly — should be denied by subnet rule.
    +try {
    +  await Deno.connect({ hostname: "127.0.0.1", port: 12345 });
    +  console.log("FAIL: direct 127.0.0.1 was not denied");
    +} catch {
    +  console.log("PASS: direct 127.0.0.1 denied");
    +}
    +
    +// Try connecting via decimal numeric hostname 2130706433 — should also be denied.
    +try {
    +  await Deno.connect({ hostname: "2130706433", port: 12345 });
    +  console.log("FAIL: numeric 2130706433 was not denied");
    +} catch {
    +  console.log("PASS: numeric 2130706433 denied");
    +}
    +
    +// Try connecting via 0x7f000001 (hex form) — should also be denied.
    +try {
    +  await Deno.connect({ hostname: "0x7f000001", port: 12345 });
    +  console.log("FAIL: hex 0x7f000001 was not denied");
    +} catch {
    +  console.log("PASS: hex 0x7f000001 denied");
    +}
    
  • tests/specs/permission/deny_net_numeric_hostname_subnet/__test__.jsonc+8 0 added
    @@ -0,0 +1,8 @@
    +{
    +  "tests": {
    +    "deny_net_numeric_hostname_subnet": {
    +      "args": "run --allow-net --deny-net=127.0.0.0/8 main.ts",
    +      "output": "main.out"
    +    }
    +  }
    +}
    
  • tests/specs/permission/deny_net_numeric_hostname/__test__.jsonc+8 0 added
    @@ -0,0 +1,8 @@
    +{
    +  "tests": {
    +    "deny_net_numeric_hostname": {
    +      "args": "run --allow-net --deny-net=127.0.0.1 main.ts",
    +      "output": "main.out"
    +    }
    +  }
    +}
    

Vulnerability mechanics

Root cause

"The Node.js compatibility TCP path checked permission against the original hostname string before DNS resolution and did not re-check the resolved IP address, allowing numeric hostname aliases to bypass --deny-net rules."

Attack vector

An attacker who can execute code inside a Deno process (e.g., via a dependency, plugin, or untrusted input) passes a numeric alias of a denied IP address — such as the decimal integer `2130706433` or hex form `0x7f000001`, both of which the OS resolver maps to `127.0.0.1` — to `node:net.connect({ host: ... })` or `node:http.request({ hostname: ... })` [ref_id=2]. The Node.js compatibility path checks the `--deny-net` rule against the original string before resolution and does not re-check the resolved IP, so the connection reaches the denied loopback destination [CWE-285]. The bypass only succeeds when the program uses a `--deny-net` denylist (rather than an `--allow-net` allowlist) and the call goes through the options-host form rather than a URL string [ref_id=1].

Affected code

The vulnerability resides in the Node.js compatibility TCP path (`node:net.connect` and `node:http.request` with options-form `{ host, port }`). On affected versions, these paths checked the permission against the **original hostname string** before DNS resolution and did **not re-check after resolution**, allowing numeric hostname aliases to bypass `--deny-net` rules [patch_id=6192605]. Native APIs (`Deno.connect`, `fetch`) and URL-string variants were not affected because they either re-checked permissions after resolution or normalized the hostname through the URL parser before checking [ref_id=1].

What the fix does

The patch introduces `check_resolved_ip_deny()` on the `UnaryPermission<NetDescriptor>` type and `check_net_resolved()` on `PermissionsContainer`, which perform a post-resolution deny check against the resolved IP address [patch_id=6192605]. These new checks are called in `op_net_connect_tcp_inner`, `op_net_connect_tls`, `op_net_send_udp`, `op_net_listen_tcp`, `net_listen_udp`, `op_net_listen_tls`, `op_node_udp_bind`, `op_node_udp_send`, and `op_quic_endpoint_connect` — meaning every networking path now verifies that the resolved IP is not denied, closing the bypass for the Node.js compatibility path that previously only checked the hostname string [patch_id=6192605].

Preconditions

  • authThe attacker must be able to execute arbitrary JavaScript code inside the Deno process (e.g., via a malicious dependency, plugin, or attacker-controlled input).
  • configThe program must use a --deny-net denylist (e.g., --deny-net=127.0.0.1 or --deny-net=127.0.0.0/8) rather than an --allow-net allowlist.
  • inputThe call must go through the Node.js compatibility options-host form (node:net.connect({ host: ... }) or node:http.request({ hostname: ... })) rather than a URL string or native Deno API.
  • inputThe attacker must supply a numeric hostname alias (decimal integer like 2130706433 or hex like 0x7f000001) that the OS resolver maps to an IP address covered by the deny rule.

Generated on Jun 16, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.