VYPR
Moderate severityNVD Advisory· Published Mar 6, 2024· Updated Aug 5, 2024

Deno's improper suffix match testing for DENO_AUTH_TOKENS

CVE-2024-27932

Description

Deno is a JavaScript, TypeScript, and WebAssembly runtime. Starting in version 1.8.0 and prior to version 1.40.4, Deno improperly checks that an import specifier's hostname is equal to or a child of a token's hostname, which can cause tokens to be sent to servers they shouldn't be sent to. An auth token intended for example[.]com may be sent to notexample[.]com. Anyone who uses DENO_AUTH_TOKENS and imports potentially untrusted code is affected. Version 1.40.0 contains a patch for this issue

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
denocrates.io
>= 1.8.0, < 1.40.41.40.4

Affected products

1

Patches

1
de23e3b60b06

fix(cli): Add IP address support to DENO_AUTH_TOKEN (#22297)

https://github.com/denoland/denoMatt MastracciFeb 6, 2024via ghsa
1 file changed · +158 8
  • cli/auth_tokens.rs+158 8 modified
    @@ -5,7 +5,13 @@ use base64::Engine;
     use deno_core::ModuleSpecifier;
     use log::debug;
     use log::error;
    +use std::borrow::Cow;
     use std::fmt;
    +use std::net::IpAddr;
    +use std::net::Ipv4Addr;
    +use std::net::Ipv6Addr;
    +use std::net::SocketAddr;
    +use std::str::FromStr;
     
     #[derive(Debug, Clone, PartialEq, Eq)]
     pub enum AuthTokenData {
    @@ -15,7 +21,7 @@ pub enum AuthTokenData {
     
     #[derive(Debug, Clone, PartialEq, Eq)]
     pub struct AuthToken {
    -  host: String,
    +  host: AuthDomain,
       token: AuthTokenData,
     }
     
    @@ -37,6 +43,78 @@ impl fmt::Display for AuthToken {
     #[derive(Debug, Clone)]
     pub struct AuthTokens(Vec<AuthToken>);
     
    +/// An authorization domain, either an exact or suffix match.
    +#[derive(Debug, Clone, PartialEq, Eq)]
    +pub enum AuthDomain {
    +  Ip(IpAddr),
    +  IpPort(SocketAddr),
    +  /// Suffix match, no dot. May include a port.
    +  Suffix(Cow<'static, str>),
    +}
    +
    +impl<T: ToString> From<T> for AuthDomain {
    +  fn from(value: T) -> Self {
    +    let s = value.to_string().to_lowercase();
    +    if let Ok(ip) = SocketAddr::from_str(&s) {
    +      return AuthDomain::IpPort(ip);
    +    };
    +    if s.starts_with('[') && s.ends_with(']') {
    +      if let Ok(ip) = Ipv6Addr::from_str(&s[1..s.len() - 1]) {
    +        return AuthDomain::Ip(ip.into());
    +      }
    +    } else if let Ok(ip) = Ipv4Addr::from_str(&s) {
    +      return AuthDomain::Ip(ip.into());
    +    }
    +    if let Some(s) = s.strip_prefix('.') {
    +      AuthDomain::Suffix(Cow::Owned(s.to_owned()))
    +    } else {
    +      AuthDomain::Suffix(Cow::Owned(s))
    +    }
    +  }
    +}
    +
    +impl AuthDomain {
    +  pub fn matches(&self, specifier: &ModuleSpecifier) -> bool {
    +    let Some(host) = specifier.host_str() else {
    +      return false;
    +    };
    +    match *self {
    +      Self::Ip(ip) => {
    +        let AuthDomain::Ip(parsed) = AuthDomain::from(host) else {
    +          return false;
    +        };
    +        ip == parsed && specifier.port().is_none()
    +      }
    +      Self::IpPort(ip) => {
    +        let AuthDomain::Ip(parsed) = AuthDomain::from(host) else {
    +          return false;
    +        };
    +        ip.ip() == parsed && specifier.port() == Some(ip.port())
    +      }
    +      Self::Suffix(ref suffix) => {
    +        let hostname = if let Some(port) = specifier.port() {
    +          Cow::Owned(format!("{}:{}", host, port))
    +        } else {
    +          Cow::Borrowed(host)
    +        };
    +
    +        if suffix.len() == hostname.len() {
    +          return suffix == &hostname;
    +        }
    +
    +        // If it's a suffix match, ensure a dot
    +        if hostname.ends_with(suffix.as_ref())
    +          && hostname.ends_with(&format!(".{suffix}"))
    +        {
    +          return true;
    +        }
    +
    +        false
    +      }
    +    }
    +  }
    +}
    +
     impl AuthTokens {
       /// Create a new set of tokens based on the provided string. It is intended
       /// that the string be the value of an environment variable and the string is
    @@ -49,7 +127,7 @@ impl AuthTokens {
             if token_str.contains('@') {
               let pair: Vec<&str> = token_str.rsplitn(2, '@').collect();
               let token = pair[1];
    -          let host = pair[0].to_lowercase();
    +          let host = AuthDomain::from(pair[0]);
               if token.contains(':') {
                 let pair: Vec<&str> = token.rsplitn(2, ':').collect();
                 let username = pair[1].to_string();
    @@ -81,12 +159,7 @@ impl AuthTokens {
       /// matching is case insensitive.
       pub fn get(&self, specifier: &ModuleSpecifier) -> Option<AuthToken> {
         self.0.iter().find_map(|t| {
    -      let hostname = if let Some(port) = specifier.port() {
    -        format!("{}:{}", specifier.host_str()?, port)
    -      } else {
    -        specifier.host_str()?.to_string()
    -      };
    -      if hostname.to_lowercase().ends_with(&t.host) {
    +      if t.host.matches(specifier) {
             Some(t.clone())
           } else {
             None
    @@ -182,4 +255,81 @@ mod tests {
         let fixture = resolve_url("https://deno.land:8080/x/mod.ts").unwrap();
         assert_eq!(auth_tokens.get(&fixture), None);
       }
    +
    +  #[test]
    +  fn test_parse_ip() {
    +    let ip = AuthDomain::from("[2001:db8:a::123]");
    +    assert_eq!("Ip(2001:db8:a::123)", format!("{ip:?}"));
    +    let ip = AuthDomain::from("[2001:db8:a::123]:8080");
    +    assert_eq!("IpPort([2001:db8:a::123]:8080)", format!("{ip:?}"));
    +    let ip = AuthDomain::from("1.1.1.1");
    +    assert_eq!("Ip(1.1.1.1)", format!("{ip:?}"));
    +  }
    +
    +  #[test]
    +  fn test_case_insensitive() {
    +    let domain = AuthDomain::from("EXAMPLE.com");
    +    assert!(
    +      domain.matches(&ModuleSpecifier::parse("http://example.com").unwrap())
    +    );
    +    assert!(
    +      domain.matches(&ModuleSpecifier::parse("http://example.COM").unwrap())
    +    );
    +  }
    +
    +  #[test]
    +  fn test_matches() {
    +    let candidates = [
    +      "example.com",
    +      "www.example.com",
    +      "1.1.1.1",
    +      "[2001:db8:a::123]",
    +      // These will never match
    +      "example.com.evil.com",
    +      "1.1.1.1.evil.com",
    +      "notexample.com",
    +      "www.notexample.com",
    +    ];
    +    let domains = [
    +      ("example.com", vec!["example.com", "www.example.com"]),
    +      (".example.com", vec!["example.com", "www.example.com"]),
    +      ("www.example.com", vec!["www.example.com"]),
    +      ("1.1.1.1", vec!["1.1.1.1"]),
    +      ("[2001:db8:a::123]", vec!["[2001:db8:a::123]"]),
    +    ];
    +    let url = |c: &str| ModuleSpecifier::parse(&format!("http://{c}")).unwrap();
    +    let url_port =
    +      |c: &str| ModuleSpecifier::parse(&format!("http://{c}:8080")).unwrap();
    +
    +    // Generate each candidate with and without a port
    +    let candidates = candidates
    +      .into_iter()
    +      .flat_map(|c| [url(c), url_port(c)])
    +      .collect::<Vec<_>>();
    +
    +    for (domain, expected_domain) in domains {
    +      // Test without a port -- all candidates return without a port
    +      let auth_domain = AuthDomain::from(domain);
    +      let actual = candidates
    +        .iter()
    +        .filter(|c| auth_domain.matches(c))
    +        .cloned()
    +        .collect::<Vec<_>>();
    +      let expected = expected_domain.iter().map(|u| url(u)).collect::<Vec<_>>();
    +      assert_eq!(actual, expected);
    +
    +      // Test with a port, all candidates return with a port
    +      let auth_domain = AuthDomain::from(&format!("{domain}:8080"));
    +      let actual = candidates
    +        .iter()
    +        .filter(|c| auth_domain.matches(c))
    +        .cloned()
    +        .collect::<Vec<_>>();
    +      let expected = expected_domain
    +        .iter()
    +        .map(|u| url_port(u))
    +        .collect::<Vec<_>>();
    +      assert_eq!(actual, expected);
    +    }
    +  }
     }
    

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

5

News mentions

0

No linked articles in our index yet.