(この記事は日本語でも読むことが出来ます。)
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.
- denoland/deno: Deno Runtime
- denoland/deno_std: Deno Standard Modules
- denoland/deno_registry2: Code of deno.land/x
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/std@$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, 2021 | Found vulnerabilities |
September 14, 2021 | Reported vulnerabilities |
September 14, 2021 | Vulnerabilities fixed |
September 15, 2021 | Checked if I can disclose details of vulnerabilities |
October 7, 2021 | Disclose approved |
November 30, 2021 | Published this article |