(この記事は日本語でも読むことが出来ます。)

Disclaimer

Deno Land Inc., which develops Deno, isn’t running bug bounty programs, so they don’t explicitly allow vulnerability assessments.

This article describes the vulnerabilities that were reported as potential vulnerabilities, using publicly available information. This was done without actually exploiting/demonstrating the vulnerabilities and it’s not intended to encourage you to perform an unauthorized vulnerability assessment.
If you find any vulnerabilities in Deno-related services/products, please report them to [email protected].1

Also, the information contained in this article may be inaccurate because the information of a vulnerability couldn’t be validated.2

TL;DR

I found a vulnerability that could be used to read arbitrary files from the system running deno.land/x, and a Code Injection in encoding/yaml of Deno.
Of these, if the vulnerability in deno.land/x was exploited, the AWS credentials used to store the module in S3 could be stolen, resulting in arbitrary package tampering in deno.land/x.

Reasons for investigation

I read the phrase Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust. on Deno’s official website, and I was curious how secure it is, so I decided to investigate it.

Investigation scope

As I didn’t have enough time to investigate all projects related to Deno, I decided to investigate the code only in the following repositories.

Investigation result

After doing some quick investigations, I found vulnerabilities in Deno itself, Deno standard modules, and the code of deno.land/x.
As the vulnerability that existed in Deno itself was a known issue3, I won’t explain it in this article, but the other two will be explained below.

Deno Standard Modules

In Deno projects, there is a module called Deno standard library apart from the Deno core.
This module is managed in a repository separate from Deno itself and is not built into Deno itself.

In this module, there is a YAML parser (encoding/yaml), and this parser had a vulnerability that allows a malicious YAML to execute arbitrary codes once it gets parsed.

Vulnerability in encoding/yaml (CVE-2021-42139)

There is an extended schema for encoding/yaml, which is not loaded by default.
This schema can be used by writing the code like below:

import {
  EXTENDED_SCHEMA,
  parse,
} from "https://deno.land/[email protected]$STD_VERSION/encoding/yaml.ts";

const data = parse(
  `
  regexp:
    simple: !!js/regexp foobar
    modifiers: !!js/regexp /foobar/mi
  undefined: !!js/undefined ~
  function: !!js/function >
    function foobar() {
      return 'hello world!';
    }
`,
  { schema: EXTENDED_SCHEMA },
);

By using this extended schema, it’s possible to embed regex, undefined, or JavaScript functions in YAML.
In these features, the code for parsing functions was as follows, and it was possible to execute arbitrary code by writing simple JavaScript as a JavaScript function.

function reconstructFunction(code: string) {
  const func = new Function(`return ${code}`)();
  if (!(func instanceof Function)) {
    throw new TypeError(`Expected function but got ${typeof func}: ${code}`);
  }
  return func;
}

This vulnerability has been fixed in this pull request by removing support for JavaScript functions from EXTENDED_SCHEMA.

deno.land/x

deno.land/x is a service that allows the hosting of Deno modules, and it’s supporting the automatic update of modules by using GitHub releases.
It’s hosting many modules, including Deno’s install script, and is a central part of the Deno ecosystem.

The automatic update feature uploads all files in the repository to deno.land/x by default.
However, if you upload the entire repository, extra files will be included and the file size will increase.
So, to address this issue, there is a parameter called subdir to specify the directory to upload.

As this parameter is appended to the path without sanitizing it properly, it was possible to read arbitrary files on the system.

Path traversal in subdir parameter

The subdir parameter is concatenated to clonePath without any changes until the following code is reached.

    // Create path that has possible subdir prefix
    const path = (subdir === undefined ? clonePath : join(
      clonePath,
      subdir.replace(
        /(^\/|\/$)/g,
        "",
      ),
    ));

The path created here is used in the following code.

    // Walk all files in the repository (that start with the subdir if present)
    const entries = [];
    for await (
      const entry of walk(path, {
        includeFiles: true,
        includeDirs: true,
      })
    ) {
      entries.push(entry);
    }

    console.log("Total files in repo", entries.length);

    const directory: DirectoryListingFile[] = [];

    await collectAsyncIterable(pooledMap(100, entries, async (entry) => {
      const filename = entry.path.substring(path.length);

      // If this is a file in the .git folder, ignore it
      if (filename.startsWith("/.git/") || filename === "/.git") return;

      if (entry.isFile) {
        const stat = await Deno.stat(entry.path);
        directory.push({ path: filename, size: stat.size, type: "file" });
      } else {
        directory.push({ path: filename, size: undefined, type: "dir" });
      }
    }));

This code adds information of all files under the path into the array named directory, and directory is used in the following:

    await collectAsyncIterable(pooledMap(65, directory, async (entry) => {
      if (entry.type === "file") {
        const file = await Deno.open(join(path, entry.path));
        const body = await Deno.readAll(file);
        await uploadVersionRaw(
          moduleName,
          version,
          entry.path,
          body,
        );
        file.close();
      }
    }));

In this code, iterate each entry of directory, check if type is file, and if so, pass file information into the uploadVersionRaw function.
And this function uploads file contents into S3 with public-read.

As you can see from these code, if subdir parameter contains values like ../../../../../../../etc, all files under the /etc directory will be uploaded.4

By using this, an attacker can specify ../../../../../../../proc/self in subdir parameter, and read /proc/self/environ, which contains all environment variables including AWS credentials.
Since this AWS credential has access to S3 buckets that are used to store modules, it was possible to tamper arbitrary packages in deno.land/x.

This vulnerability has been fixed in this pull request by normalizing the subdir parameter.
After fixing this vulnerability, the Deno development team investigated the access log of the AWS credentials and confirmed that this vulnerability was not exploited in wild.

Conclusion

In this article, I described vulnerabilities that were existed in Deno projects.
From these cases, you can see that it is difficult to prevent vulnerabilities, no matter how much the project is trying to be secure.

If you have questions or comments, please send them on Twitter (@ryotkak).

Timeline

Date (JST)Event
September 13, 2021Found vulnerabilities
September 14, 2021Reported vulnerabilities
September 14, 2021Vulnerabilities fixed
September 15, 2021Checked if I can disclose details of vulnerabilities
October 7, 2021Disclose approved
November 30, 2021Published this article

  1. https://github.com/denoland/deno/issues/12058 ↩︎

  2. I’ve confirmed with Deno Land Inc., but I can’t guarantee complete accuracy. ↩︎

  3. https://github.com/denoland/deno/issues/9607 ↩︎

  4. Actually, it seems that this process will be interrupted due to problems such as permissions, but the files uploaded up to that point will be retained on S3 as they are. ↩︎