CVE-2024-37150 (Other, published 2024-06-06T16:15Z)
Description: value: An issue in `.npmrc` support in Deno 1.44.0 was discovered where Deno would send `.npmrc` credentials for the scope to the tarball URL when the registry provided URLs for a tarball on a different domain. All users relying on .npmrc are potentially affected by this vulnerability if their private registry references tarball URLs at a different domain. This includes usage of deno install subcommand, auto-install for npm: specifiers and LSP usage. It is recommended to upgrade to Deno 1.44.1 and if your private registry ever serves tarballs at a different domain to rotate your registry credentials.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/denoland/deno
Files changed:
Cargo.lock
cli/Cargo.toml
cli/args/mod.rs
cli/http_util.rs
cli/npm/managed/cache/tarball.rs
tests/integration/lsp_tests.rs
tests/registry/npm-private/@denotest/tarballs-privateserver2/1.0.0/index.js
tests/registry/npm-private/@denotest/tarballs-privateserver2/1.0.0/package.json
tests/registry/npm-private2/@denotest/tarballs-privateserver2/1.0.0/index.js
tests/registry/npm-private2/@denotest/tarballs-privateserver2/1.0.0/package.json
tests/registry/npm/@denotest/tarballs-privateserver2/1.0.0/index.js
tests/registry/npm/@denotest/tarballs-privateserver2/1.0.0/package.json
tests/specs/compile/npmrc/.npmrc
tests/specs/compile/npmrc/install.out
tests/specs/npm/npmrc/.npmrc
tests/specs/npm/npmrc/install.out
tests/specs/npm/npmrc_bad_registry_config/.npmrc
tests/specs/npm/npmrc_bad_registry_config/main.out
tests/specs/npm/npmrc_bad_token/.npmrc
tests/specs/npm/npmrc_bad_token/main.out
tests/specs/npm/npmrc_basic_auth/.npmrc
tests/specs/npm/npmrc_basic_auth/install.out
tests/specs/npm/npmrc_deno_json/.npmrc
tests/specs/npm/npmrc_deno_json/main.out
tests/specs/npm/npmrc_not_next_to_package_json/.npmrc
tests/specs/npm/npmrc_tarball_other_server/__test__.jsonc
tests/specs/npm/npmrc_tarball_other_server/fail/.npmrc
tests/specs/npm/npmrc_tarball_other_server/fail/main.js
tests/specs/npm/npmrc_tarball_other_server/fail/main.out
tests/specs/npm/npmrc_tarball_other_server/fail/package.json
tests/specs/npm/npmrc_tarball_other_server/success/.npmrc
tests/specs/npm/npmrc_tarball_other_server/success/main.js
tests/specs/npm/npmrc_tarball_other_server/success/main.out
tests/specs/npm/npmrc_tarball_other_server/success/package.json
tests/util/server/src/npm.rs
Commit message: fix(npm): use configured auth for tarball urls instead of scope auth (#24111)

Deno was using the scope auth for the tarball urls, which is not always
correct. We are going to do a release immediately for this issue.

Vulnerable code:
version = "0.21.1"
checksum = "0971210ea837a8153c2e1bec7d65aabf372f436845e576db95c596c0f4b1209d"
deno_npm = "=0.21.1"
use deno_core::anyhow::bail;
  ) -> Result<Option<Vec<u8>>, AnyError> {
  ) -> Result<Option<Vec<u8>>, AnyError> {
      bail!(
        "Bad response: {:?}{}",
        status,
        match maybe_response_text {
          Some(text) => format!("\n\n{text}"),
          None => String::new(),
        }
      );
  ) -> Result<reqwest::Response, AnyError> {
      Err(custom_error("Http", "Too many redirects."))
) -> Result<Vec<u8>, AnyError> {
) -> Result<Url, AnyError> {
    Err(generic_error(format!(
      "Redirection from '{request_url}' did not provide location header"
    )))
      let registry_config =
        tarball_cache.npmrc.get_registry_config(&package_nv.name).clone();
      let maybe_auth_header =
        maybe_auth_header_for_npm_registry(&registry_config);
      let maybe_bytes = tarball_cache.http_client_provider
        .download_with_progress(&dist.tarball, maybe_auth_header, &guard)
        .await?;
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token
@denotest2:registry=http://127.0.0.1:4262/
//127.0.0.1:4262/:_authToken=private-reg-token2
Download http://127.0.0.1:4261/@denotest/basic
Download http://127.0.0.1:4262/@denotest2/basic
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token
@denotest2:registry=http://127.0.0.1:4262/
//127.0.0.1:4262/:_authToken=private-reg-token2
Download http://127.0.0.1:4261/@denotest/basic
Download http://127.0.0.1:4262/@denotest2/basic
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/denotest/:_authToken=invalid-token
Download http://127.0.0.1:4261/@denotest/basic
error: Error getting response at http://127.0.0.1:4261/@denotest/basic for package "@denotest/basic": Bad response: 401
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=invalid-token
Download http://127.0.0.1:4261/@denotest/basic
error: Error getting response at http://127.0.0.1:4261/@denotest/basic for package "@denotest/basic": Bad response: 401
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_auth=ZGVubzpsYW5k
@denotest2:registry=http://127.0.0.1:4262/
//127.0.0.1:4262/:_auth=ZGVubzpsYW5kMg==
Download http://127.0.0.1:4261/@denotest/basic
Download http://127.0.0.1:4262/@denotest2/basic
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token
Download http://127.0.0.1:4261/@denotest/basic
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token

Fixed code:
version = "0.21.2"
checksum = "5c7ae566a3cba78bf05751c20708f28385fe339b1d07bd8daff16316317d4228"
deno_npm = "=0.21.2"
    registry_configs: Default::default(),
use thiserror::Error;
#[derive(Debug, Error)]
#[error("Bad response: {:?}{}", .status_code, .response_text.as_ref().map(|s| format!("\n\n{}", s)).unwrap_or_else(String::new))]
pub struct BadResponseError {
  pub status_code: StatusCode,
  pub response_text: Option<String>,
}

#[derive(Debug, Error)]
pub enum DownloadError {
  #[error(transparent)]
  Reqwest(#[from] reqwest::Error),
  #[error(transparent)]
  ToStr(#[from] reqwest::header::ToStrError),
  #[error("Redirection from '{}' did not provide location header", .request_url)]
  NoRedirectHeader { request_url: Url },
  #[error("Too many redirects.")]
  TooManyRedirects,
  #[error(transparent)]
  BadResponse(#[from] BadResponseError),
}

  ) -> Result<Option<Vec<u8>>, DownloadError> {
  ) -> Result<Option<Vec<u8>>, DownloadError> {
      return Err(DownloadError::BadResponse(BadResponseError {
        status_code: status,
        response_text: maybe_response_text
          .map(|s| s.trim().to_string())
          .filter(|s| !s.is_empty()),
      }));
      .map_err(Into::into)
  ) -> Result<reqwest::Response, DownloadError> {
      Err(DownloadError::TooManyRedirects)
) -> Result<Vec<u8>, reqwest::Error> {
) -> Result<Url, DownloadError> {
    Err(DownloadError::NoRedirectHeader {
      request_url: request_url.clone(),
    })
use reqwest::StatusCode;
use reqwest::Url;
use crate::http_util::DownloadError;
      // IMPORTANT: npm registries may specify tarball URLs at different URLS than the
      // registry, so we MUST get the auth for the tarball URL and not the registry URL.
      let tarball_uri = Url::parse(&dist.tarball)?;
      let maybe_registry_config =
        tarball_cache.npmrc.tarball_config(&tarball_uri);
      let maybe_auth_header = maybe_registry_config.and_then(|c| maybe_auth_header_for_npm_registry(c));
      let result = tarball_cache.http_client_provider
        .download_with_progress(tarball_uri, maybe_auth_header, &guard)
        .await;
      let maybe_bytes = match result {
        Ok(maybe_bytes) => maybe_bytes,
        Err(DownloadError::BadResponse(err)) => {
          if err.status_code == StatusCode::UNAUTHORIZED
            && maybe_registry_config.is_none()
            && tarball_cache.npmrc.get_registry_config(&package_nv.name).auth_token.is_some()
          {
            bail!(
              concat!(
                "No auth for tarball URI, but present for scoped registry.\n\n",
                "Tarball URI: {}\n",
                "Scope URI: {}\n\n",
                "More info here: https://github.com/npm/cli/wiki/%22No-auth-for-URI,-but-auth-present-for-scoped-registry%22"
              ),
              dist.tarball,
              registry_url,
            )
          }
          return Err(err.into())
        },
        Err(err) => return Err(err.into()),
      };
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
module.exports = () => 'hi_private1';
{
  "name": "@denotest/tarballs-privateserver2",
  "version": "1.0.0"
}
module.exports = () => 'hi_private2';
{
  "name": "@denotest/tarballs-privateserver2",
  "version": "1.0.0"
}
// this is a special package that the test server serves tarballs from the second private registry server
module.exports = () => 'hi';
{
  "name": "@denotest/tarballs-privateserver2",
  "version": "1.0.0"
}
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
@denotest2:registry=http://localhost:4262/
//localhost:4262/:_authToken=private-reg-token2
Download http://localhost:4261/@denotest/basic
Download http://localhost:4262/@denotest2/basic
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
@denotest2:registry=http://localhost:4262/
//localhost:4262/:_authToken=private-reg-token2
Download http://localhost:4261/@denotest/basic
Download http://localhost:4262/@denotest2/basic
@denotest:registry=http://localhost:4261/
//localhost:4261/denotest/:_authToken=invalid-token
Download http://localhost:4261/@denotest/basic
error: Error getting response at http://localhost:4261/@denotest/basic for package "@denotest/basic": Bad response: 401
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=invalid-token
Download http://localhost:4261/@denotest/basic
error: Error getting response at http://localhost:4261/@denotest/basic for package "@denotest/basic": Bad response: 401
@denotest:registry=http://localhost:4261/
//localhost:4261/:_auth=ZGVubzpsYW5k
@denotest2:registry=http://localhost:4262/
//localhost:4262/:_auth=ZGVubzpsYW5kMg==
Download http://localhost:4261/@denotest/basic
Download http://localhost:4262/@denotest2/basic
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
Download http://localhost:4261/@denotest/basic
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
{
  "tempDir": true,
  "tests": {
    "auth_success": {
      "cwd": "success",
      "args": "run --node-modules-dir -A main.js",
      "output": "success/main.out",
      "exitCode": 1
    },
    "auth_fail": {
      "cwd": "fail",
      "args": "run --node-modules-dir -A main.js",
      "output": "fail/main.out",
      "exitCode": 1
    }
  }
}
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
import getValue from "@denotest/tarballs-privateserver2";

console.log(getValue());
Download http://localhost:4261/@denotest/tarballs-privateserver2
Download http://localhost:4262/@denotest/tarballs-privateserver2/1.0.0.tgz
error: Failed caching npm package '@denotest/tarballs-privateserver2@1.0.0'.

Caused by:
    No auth for tarball URI, but present for scoped registry.
    
    Tarball URI: http://localhost:4262/@denotest/tarballs-privateserver2/1.0.0.tgz
    Scope URI: http://localhost:4261/
    
    More info here: https://github.com/npm/cli/wiki/%22No-auth-for-URI,-but-auth-present-for-scoped-registry%22
{
  "name": "npmrc_test",
  "version": "0.0.1",
  "dependencies": {
    "@denotest/tarballs-privateserver2": "1.0.0"
  }
}
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
//localhost:4262/:_authToken=private-reg-token2
import getValue from "@denotest/tarballs-privateserver2";

console.log(getValue());
Download http://localhost:4261/@denotest/tarballs-privateserver2
Download http://localhost:4262/@denotest/tarballs-privateserver2/1.0.0.tgz
[# This fails on a checksum issue, because the test server isn't smart enough]
[# to serve proper checksums for a package at another registry. That's fine]
[# though because this shows us that we're making it to this step instead of]
[# failing sooner on an auth issue.]
error: Failed caching npm package '@denotest/tarballs-privateserver2@1.0.0'.

Caused by:
    Tarball checksum did not match [WILDCARD]
{
  "name": "npmrc_test",
  "version": "0.0.1",
  "dependencies": {
    "@denotest/tarballs-privateserver2": "1.0.0"
  }
}
  let registry_hostname = if package_name == "@denotest/tarballs-privateserver2"
  {
    "http://localhost:4262"
  } else {
    registry_hostname
  };