This is a continuation of Idea: Security advisories as part of crates.io metadata. The following text is a copy-paste of https://github.com/untitaker/rfcs/blob/security-advisories/text/0000-security-advisories.md, the text on GitHub and this post will be updated as the discussion progresses.
Since we’ve already had relatively much of discussion about this, please tell me if this is already ready for a pull request.
Summary
Crates.io should offer an API to release security advisories for crates. Cargo should offer a new command for the same purpose and warn about vulnerable crate versions during compilation.
Motivation
When compared to other ecosystems such as Python’s, Rust’s broader community prefers many single-purpose crates over larger monoliths. This situation, together with the strongly encouraged practice of pinning MINOR versions of dependencies, slows down the propagation of critical security fixes.
Assume a crate W
, which depends on X
, which depends on Y
, which depends
on Z
. If Z
releases a new MINOR version including a security fix, it
requires the attention of Y
's and X
's maintainers to propagate that
security fix to W
. What makes this situation worse is that the author of W
is never notified that they were running a vulnerable version of Z
all the
time.
Tooling that builds on top of the API provided by Crates.io (such as Cargo) could alert crate users of their vulnerabilities, which in turn spurs them to update their dependencies accordingly. Even if that does not happen, the additional metadata at least makes it clear which crates are potentially dangerous to use and which ones not. This not only helps Rust programmers, but potentially also distributors (such as packagers of Linux distros) and end-users.
Detailed design
Crates.io
Similar to yanking, Crates.io should provide an API that allows a user of Cargo to attach an arbitrary amount of so-called security advisories to crates they own.
Each advisory gets assigned an ID that is unique within the set of advisories
for the affected crate. Every advisory should have a unique URL, for example
https://crates.io/crates/<crate>/advisory/<id>
, where <id>
is the
advisory’s ID. On that URL a human-readable representation of the advisory
should be stored.
Other pages on Crates.io should link to those advisories prominently where appropriate.
Cargo
cargo advisory
A command called advisory
will be added to Cargo. Here is an excerpt of its help
page:
$ cargo advisory --help
Generate and upload security advisories for the given or the current crate.
Usage:
cargo advisory [options] -- [<crate>]
--filename PATH The filename to use. Defaults to `./Advisory.toml`.
If `-` is given, generated advisories are printed to
stdout and advisories to upload are read from stdin.
--vers VERSION Versions to release this advisory for. Can be
specified multiple times. Only valid in conjunction
with --generate.
--upload/--generate Whether to upload or generate a advisory. The default
is to generate. These options are mutually exclusive.
[...]
Like yank
it takes a --vers
option, with two differences:
-
if a version is not specified,
advisory
will default to all existing versions. -
Version ranges such as
<1.2.6, >1.0.0
can be specified. This is comparable to the syntax used for specifying dependencies in theCargo.toml
, with the exception thatx.y.z
is not equivalent to^x.y.z
, but means the exact version.
Here’s the workflow:
-
The user invokes
cargo advisory
without the--upload
option. Cargo will generate a file underfilename
. Cargo should abort if the file already exists. The content looks like this:[vulnerability] package = "mypackage" versions = ["1.2.0", "1.2.3", "1.2.4", "1.2.5"] # It is strongly recommended to request a CVE, or alternatively a DWF, and # reference the assigned number here. # - CVE: https://iwantacve.org/ # - DWF: https://distributedweaknessfiling.org/ dwf = false # dwf = "CVE-YYYY-XXXX" # dwf = ["CVE-YYYY-XXXX", "CVE-ZZZZ-WWWW"] # URL to a long-form description of this issue, e.g. a blogpost announcing # the release or a changelog entry (optional) url = false # Enter a short-form description of the vulnerability here. Preferrably a # single paragraph (required) description = """ """
-
The user invokes
cargo advisory --upload
. Cargo verifies the passed file against the following rules:- the file exists and is valid TOML
- Optional keys may be either
false
or absent. - the
description
contains not only whitespace. More text than a paragraph should be allowed, but not necessarily recommended. -
package
exists on Crates.io and the versions specified inversions
exist -
dwf
is not an empty array. It should befalse
if there are none.
If not, Cargo should print one or more error messages and exit.
-
When the advisory is found to be valid, Cargo should print a summary, ask the user for confirmation and upload it to the package index. The vulnerability ID assigned by Crates.io and optionally the corresponding URL should be printed to stdout.
The recommended workflow is to first file the advisory with cargo advisory
,
and then release the versions that contain the security fix.
Using vulnerable packages
-
cargo build
andcargo install
will emit a warning for each vulnerable package used, regardless of whether this package is already compiled, downloaded or otherwise cached, or whether it is a direct dependency or not:Downloading foo vx.y.z Downloading bar vx.y.z Warning: bar vx.y.z (dependency of foo vx.y.z) is vulnerable. See https://crates.io/... for details.
-
cargo publish
will refuse to upload a crate if any version of a direct dependency satisfying the constraints inCargo.toml
is vulnerable. Indirect dependencies should not trigger this behavior.For example, if I have a dependency such as
bar = "^1.2.3"
, this meanspublish
should refuse to upload my crate even ifbar=1.2.3
is not vulnerable, as another version satisfying that constraint may be.
The author of a crate that directly depends on a vulnerable crate may disable
these warnings with a switch in their Cargo.toml
. If iron==0.4.x
has
an advisory with the ID deadbeef
, the dependent author may use the
allow_vulnerable
parameter to disable all the above-described warnings and
errors for this vulnerability:
[dependencies]
iron = { version = "0.4", allow_vulnerable = ["deadbeef"] }
This only affects the warnings for deadbeef
for the current crate. Cargo will
still print warnings:
- for other vulnerabilities. Each warning has to be explicitly disabled by appending its ID to that array.
- if another package in the dependency graph uses a version of
iron
that has thedeadbeef
vulnerability, but does not haveallow_vulnerable = ["deadbeef"]
set.
Cargo must reject nonexistent vulnerability IDs with a fatal error.
Drawbacks
There is a risk that users will abuse this system to mark their versions as deprecated or to call out other kinds of critical bugs such as data loss. This would make the entire advisory system as semantically worthless.
Alternatives
Extending yanking for security advisories
It has been proposed to extend the semantics of yanking such that it could be used for security advisories. While this alternative meets the popular aesthetic preference of having generic commands with a large variety of usecases (over single-purpose commands), using yanking this way has a few drawbacks:
-
Cargo dosen’t allow yanked packages to be used in new packages. In the author’s opinion, people who know what they’re doing should be allowed to depend on vulnerable packages, as they might use the crate in a way that poses no security threat.
Some vulnerabilities can be mitigated in ways other than upgrading a crate, like making local configuration changes. Some vulnerabilities may affect optional functionality not everyone is using, or functionality that can be compiled out by e.g. disabling certain cargo feature settings for that crate. Some may be relatively innocuous and/or hard-to-exploit and therefore not warrant an immediate upgrade. Sometimes no action (other than setting
allow_vulnerable = true
) is required at all because the dependent crate never used the vulnerable functionality to begin with.At the same time it doesn’t make sense to depend on packages that don’t compile, and currently yanking is primarily used to mark such packages.
-
Cargo doesn’t give any advice about further procedure when yanking a package. I think in the context of security vulnerabilities this is very much needed, as few OSS maintainers are exposed to this problem regularly enough to know what they’re doing.
-
Lastly, the data exposed via the Crates.io would be a lot less structured. Automatic security notifications via third-party tooling would be impossible because there is no way to determine whether a package was yanked because of a security vulnerability or not.
Most of these problems can be fixed by asking the user to attach a “reason” to
their yanked packages, such as security
, deprecation
, broken
(and
then make Cargo’s behavior dependent on that). However, at that that point
yank
is no longer generic (as in a function having type parameters), but
simply a lot of single-purpose commands stuffed into one (as in function
overloading in Java). And the name “yank” wouldn’t make sense for crate
versions that may still be available (depending on the “reason”).
Unresolved questions
DWF vs CVE
- It may be counterintuitive that one can specify CVEs in the DWF parameter.
Should it be called
cve
instead even though it can also be used for DWFs?
Comparison:
- CVEs are more popular
- Applying for a CVE number is a manual process and requires review by a human. DWFs can be automatically managed assigned by Crates.io
What to do if dwf = false
-
Crates.io could apply for blocks of DWF IDs and automatically assign them if
the user didn’t specify one in the advisory (
dwf = false
).
CVSS
- Scoring vulnerabilities. Should a new field for the usage of CVSS be created?