Dependency confusion attack — may be applicable to alternative registries

Here's an interesting hack:

  1. guess/find names of private packages used internally within companies,
  2. create public packages with the same names,
  3. wait until someone or some tool uses the public one by accident.

Unfortunately, this problem may be applicable to Cargo too! If someone wrote:

[dependencies]
company-internal = "1.0"

instead of

[dependencies]
company-internal = { version = "1.0", registry = "corpo" }

they could end up fetching and executing untrusted code.

11 Likes

Doesn't really look like there's anything particularly actionable with regard to Rust, correct? Cargo doesn't have the "use the higher version" issue.

Indeed. I don't see any technical vulnerability in the case of Cargo. There is only the human aspect if someone forgets the registry property in Cargo.toml or uses cargo add without --registry.

I don't know if Cargo could do better here. Maybe it could warn or require disambiguation when told to fetch a dependency from a public repository when a private repository has a crate with the same name?

4 Likes

Can the crates.io registry be disabled or made non-default? Companies that want to audit their dependencies could then mirror the crates that have been manually approved, and any confusion attack would need to survive their audit process.

1 Like

Yes, it can be replaced by a defined substitute. From The Cargo Book :-

# The crates.io default source for crates is available under the name
# "crates-io", and here we use the `replace-with` key to indicate that it's
# replaced with our source above.
[source.crates-io]
replace-with = "my-vendor-source"

This config goes in ".cargo/config.toml" which could also be configured at a user profile rather than project level (e.g. $HOME/.cargo/config.toml).

5 Likes

Hopefully their build environments are disabling or highly restricting internet access generally as well.

We're using cargo-deny to manage which crates/versions/licenses/registries are allowed. Doesn't solve the problem fully though.

The last step that is missing (for us at least) is to be able to use Artifactory/Nexus/something else as a hosted private repo which can do proxying to public repos. Projects would then only have access to the private repo, public access would be removed.

Looks like Artifactory will have Rust support soon: https://www.jfrog.com/jira/browse/RTFACT-13469

Edit: Just checked and turns out we don't rely on cargo-deny to check repos, and for internal crates we use URL to the internal crate itself, the same as we do with internal Python deps.

I'm afraid that Cargo can't help here without a breaking change. Alternative registries are stable, and could already contain crates with the same name as crates.io crates (very likely if they're private forks). Treating such ambiguity as a hard error would be a breaking change. OTOH only displaying a warning would do very little, because a warning won't stop potentially malicious build scripts from running.

So for now I think the best that we can do is to make cargo-edit detect this, and when it becomes officially part of Cargo, steer users towards using it over editing Cargo.toml by hand:

1 Like

Breaking changes to cargo can happen along with edition upgrades, iirc. It was (is?) being discussed for changing the default feature resolver.

1 Like

Having it warn for such conflicts with an option to fail hard (e.g. configured through ~/.cargo/config) seems like a reasonable enough workaround

4 Likes

Security issues can't simply be handwaved away by saying backwards compatibility. If an SSL libraru didn't properly verify its keys, doing so now would definitely be a break in backwards compat, but also the thing that should be done.

1 Like

Strictly speaking this isn't a fix for a vulnerability, but an addition of a lint that may catch a human error.

I want to be very clear that Cargo is different from the package managers that enabled the dependency confusion attack. These other package managers automatically selected wrong registries under certain conditions. Cargo doesn't do that.

7 Likes

The registry keyword to specify a private registry in a dependency (e.g. company-internal = { version = "1.0", registry = "corpo" }) is sufficient, but it would be useful if support for a private registry were more explicit. Say, by having a [private_dependencies] section in Cargo.toml and a mechanism to configure the registry within the same file, instead of having to do so in .cargo/config.toml.

If Cargo were to go down that path would a better option be to allow specification of the registry at section level. In other words instead of having [private_dependencies] you would have :-

[registry.'corpo'.dependencies]
company-internal = "1.0"
6 Likes