Hacker News new | ask | show | jobs
by klodolph 1534 days ago
Discussing "what is a lockfile" is a bit of a headache because different languages have different files which do different things. Generally speaking, there's some file which specifies the dependency versions and some file with cryptographic checksums of the all transitive dependencies.

In Go it's go.mod / go.sum. In NPM, it's package.json / package-lock.json. In Rust it's Cargo.toml / Cargo.lock.

Diving into the exact details of what the author is saying is a bit outside my headspace at the moment. I think the author of the article may not actually understand the scenario where Go's package system differs. (I'm not sure I do, either.)

Suppose you have your project, projectA, and its direct dependency, libB. Then libB has a dependency on libC.

If projectA has a lockfile, you get exactly the same versions of libA and libB. This is true for Go, NPM, and Cargo. However, suppose projectA is a new project. You just created it. In Go, the version of libB that makes it into the lockfile will be the minimum version that libA requires, which means that any new, poisoned version of libB will not transitively affect anything that depends on libA, such as projectA. With NPM, you get the latest version of libB which is compatible with libA--this version may be poisoned.

1 comments

> any new, poisoned version of libB

Conversely, you will get any old security-buggy version of libB instead.

Most package managers when adding a new dependency assume newer versions are "better" than older versions. Go's minimum version system assumes older is better than newer.

I don't think there's any clear argument you can make on first principles for which of those is actually the case. You'd probably have to do an empirical analysis of how often mailicious packages get published versus how often security bug fix versions get published. If the former is more common than the latter, then min version is likely a net positive for security. If the latter is more common than the former, then max version is probably better. You'd probably also have to evaluate the relative harm of malicious versions versus unintended security bugs.

> I don't think there's any clear argument you can make on first principles for which of those is actually the case.

I don't understand why someone would try to argue from first principles here, it just seems like such a bizarre approach.

Anyway, it's not just a security issue. Malicious packages and security fixes are only part of the picture. Other issues:

- Despite a team's promise to use semantic versioning, point releases & "bugfix" releases will break downstream users

- Other systems for determining the versions to use are much more unpredictable and hard to understand than estimated (look at Dart and Cargo)

https://github.com/dart-lang/pub/blob/master/doc/solver.md

https://github.com/rust-lang/cargo/blob/1ef1e0a12723ce9548d7...

> Other systems for determining the versions to use are much more unpredictable and hard to understand than estimated (look at Dart and Cargo)

I'm one of the co-authors of Dart's package manager. :)

Yes, it is complex. Code reuse is hard and there's no silver bullet.

Nice! I hope I wasn't coming across as critical of Dart's package manager, or Cargo for that matter.
It's OK. There are always valid criticisms of all possible package managers. It's just a hard area with gnarly trade-offs.
Every change that fixes a security issue implies the existence of a change that introduced the security issue in the first place. Why is bumping a version more likely to remove security issues instead of introduce them?

The reason why older is better than newer has more to do with the fact that the author has actually tested their software with that specific version, and so there's more of a chance that it actually works as they intended.

Security issues aren't introduced intentionally, oftentimes they are found much later on in code that was assumed to be secure. Like the SSL heartbleed vulnerability. Once a vulnerability like that is discovered, you _want_ every developer to update their deps to the most secure version
My statement had nothing to do with intent. Conversely, once a vulnerability is introduced (intentionally or not), you don't want every developer to update their deps to the newly insecure version.
Exactly, so it's a trade-off, do you want to encourage updates at the risk of malicious updates (like with node-ipc). Or do you want to add friction to updates and thus risk security vulnerabilities persisting for longer. Node chooses one approach, Go chooses the other.
Again, it's not just malicious updates. Normal updates can also introduce security vulnerabilities. For example, I have a dependency at v1.0 and v1.0.1 introduces a security bug unintentionally. It is eventually fixed in v1.1. If I wait to update until v1.1, then I am not vulnerable to that bug whereas an automatic update to v1.0.1 would be vulnerable. My point is that in expectation, updating your dependency could be just as likely to remove a security vulnerability as it is to add one.
I'd back that down to "most security issues aren't introduced intentionally".
Go just expects you to manually trigger the updates. Thats all. It still is in favor of updating to take security fixes, so i think your argument is wrong.
Let's say my_app uses package foo which uses package bar.

It turns out there is a security bug in bar. The bar maintainers release a patch version that fixes it.

In most package managers, users of my_app can and will get that fix with no work on the part of the author of foo. I'm not very familiar with Go's approach but I thought that unless foo's author puts out a version of foo that bumps its minimum version dependency on bar, my_app won't get the fix. Version solving will continue to select the old buggy version of bar because that's the minimum version the current version of foo permits.

> I thought that unless foo's author puts out a version of foo that bumps its minimum version dependency on bar, my_app won't get the fix. Version solving will continue to select the old buggy version of bar because that's the minimum version the current version of foo permits.

That is incorrect. The application's go.mod defines all dependencies, even indirect ones. Raise the version there, and you raise it for all dependencies. You cannot have one more than one minor version of a dependency in the dependency graph.

That still implies that I need to know to update the version constraint of what may be a very deep transitive dependency, doesn't it?
The version constraint is always listed in your top level go.mod file, so you know the dependency exists, no digging into the dependency tree required at all, and it’s not hidden in some lock file no one ever looks at. Plus, there are plenty of tools that help you with this problem, including the language server helping you directly in your editor and Dependabot on GitHub.

I’m not aware of any languages that send you an email when your dependencies are out of date, so yes, you need to check them. Dependabot can do this for you and open a PR automatically, which will result in an email, so this is one way for people to stay on top of this stuff even for projects they deploy but don’t work on every single week.

If you’re suggesting that indirect dependencies should automatically update themselves, then you are quite literally saying those code authors should have a shell into your production environments that you have no control over, compromising all your systems with a single package update that no one but the malicious author got to review. It is possible with tools like Dependabot to be notified proactively when updates are required so you can review and apply those, but it is not possible to go back in time and un-apply a malicious update that went straight to prod.

Repeatedly assuming that the Go core team never thought through the design of Go Modules and how it relates to security updates is such a strange choice. Go is a very widely used language with tons of great tooling.

As I understand it, that's true in the simple case. If you have `my_app -> foo -> bar` then there's only one path to bar, and you only get a new bar when you upgrade foo.

It's more complicated in general, with diamond dependencies. There needs to be a chain of module updates between you and foo, with the minimum case being a chain of length one where you specify the version of foo directly.

So, people do need to pay attention to security patch announcements. But popular modules, at least, are likely to be get updated relatively quickly, because only one side of a diamond dependency needs to notice and do a release.

> As I understand it, that's true in the simple case. If you have `my_app -> foo -> bar` then there's only one path to bar, and you only get a new bar when you upgrade foo.

This is not correct. You can update bar independent of foo directly from the top-level go.mod file in your project.

Yes, you can do that by adding a direct dependency on foo. I started by talking about when there isn't a direct dependency on foo.

I explicitly talked about having a direct dependency at the end of the second paragraph.

You keep harping on old buggy version where Go has been very clear that it is operator's explicit responsibility to have correct/updated/fixed versions of dependency running.

It specially does not look good in your case considering you work for Google on a different programing language. If you have a clear point to make then compare it with your approach. Instead of making neutral sounding arguments when they are not.

Don't drag in ad-hominem attacks. If you want to defend Go's approach, explain why having it be the "operator's explicit responsibility" is a good policy, likely to make apps (in general) more secure. The obvious implication of the example given is that, on average, it will be a mess.
The code itself isn’t the only risk factor; it’s weighted by others, like discoverability, which are asymmetric in time. If the white hats fix an issue in version n+1, they’re going to make sure people know. If black hats (or normal devs making a mistake) introduce an issue, no one will tell you about it.

I.e. even if both strategies win just as often, min-version pulls ahead by taking less of a hit from losses.