Comment by jerf

Comment by jerf 9 days ago

42 replies

No.

Ironically, a flip side of the complaints about how Go lacks power is that a lot of the "standard" security vulnerabilities actually become harder to write. The most obvious one is lacking the "eval" that a dynamic language has; more subtle ones include things like, there is no way to take a string and look up a type or a method in the runtime, so things like the Ruby YAML vuln are not assisted by the language level. To write something like that into Go, you'd have to actually write it in. Though you can, if you try hard enoough.

But, as sibling comments point out, nothing stops you from writing an SQL injection. Command injections are inhibited by the command only taking the "array of strings" form of a command, with no "just pass me a string and we'll do shell things to it" provided by the language, but I've dispatched multiple questions about how to run commands correctly in Go by programmers who managed to find []string{"bash", "-c", "my command with user input from the web here"}, so the evidence suggests this is still plenty easy enough to write. Putting the wrong perms or no perms on your resources is as easy as anything else; no special support for internal security (compare with E lang and capabilities languages). And the file access is still based on file names rather than inodes, so file-based TOCTOUs are the default in Go (just like pretty much everywhere else) if you aren't careful. It comes with no special DOS protection or integrated WAF or anything else. You can still store passwords directly in databases, or as their MD5 sums. The default HTML templating system is fairly safe but you can still concatenate strings outside of the template system and ship them out over an HTTP connection in bad ways. Not every race condition is automatically a security vulnerability, but you can certainly write race conditions in Go that could be security vulnerabilities.

I'd say Go largely lacks the footguns some other languages have, but it still provides you plenty of knives you can stab yourself with and it won't stop you.

I've been running govulncheck against my repos for a while, and I have seen some real vulnerabilities go by that could have affected my code, but rather than "get arbitrary execution" they tend to be "didn't correctly escape output in some particular edge case", which in the right circumstances can still be serious, but is still at least less concerning than "gets arbitrary execution".

Smaug123 9 days ago

> I'd say Go largely lacks the footguns some other languages have

With the glaring exception of "I forgot to check the error code", which you need a linter (e.g. as provided by golangci-lint) for. It's critically important for security that you know whether the function you just called gave you a meaningful result! Most other languages either have sum types or exceptions.

  • tptacek 9 days ago

    No it's not. This is what I meant, cross-thread, when I suggested being wary of arguments trying to draw significant distinctions between memory-safe-language X and memory-safe-language Y. Error checking idioms and affordances have profound implications for correctness and for how you build and test code. Programmers have strong preferences. But those implications have only incidental connections to security, if any. Nevertheless "security" is a good claim to throw into a "my language is better" argument.

    • Smaug123 9 days ago

      I don't even use Golang, I maybe read two Golang repos a year, I find these errors in almost every repo I look at (probably because of the selection effect: I only look at the code for tools I find bugs in). One of them I remember was a critical vulnerability of exactly this form, so :shrug: Perhaps I'm just grotesquely unlucky in the Golang projects I see, but that makes maybe 10% of the Golang error-handling bugs I've found to be security bugs.

      • tptacek 9 days ago

        Sounds memorable. Say more about this critical vulnerability?

  • jerf 9 days ago

    Mmm, that's fair. I tend to forget about it because it's not something I personally struggle with but that doesn't mean it's not a problem.

    I'd still rate it well below a string eval or a default shell interface that takes strings and treats them like shell does. You assert down below that you've seen this lead to a critical vulnerability and I believe you, but in general what happens if you forget to check errors is that sooner or later you get a panic or something else that goes so far off the rails that your program crashes, not that you get privs you shouldn't. As I say in another comment, any sort of confusing bit of code in any language could be the linchpin of some specific security vulnerability, but there are still capabilities that lead to more security issues than some other capabilities. Compared to what I've seen in languages like Perl this is still only "medium-grade" at best.

    And I'm not trying to "defend" Go, which is part of why I gave the laundry list of issues it still has. It's just a matter of perspective; even missing the odd error check here or there is just not the same caliber problem as an environment where people casually blast user-sourced input out to shell because the language makes it easier than doing it right.

    (Independent of language I consider code that looks like

        operation = determineOperation()
        if !canIDoOperation(operation) {
            // handle failures
        }
        doOperation(operation)
    
    architecturally broken anyhow. It seems natural code to write, but this is a form of default allow. If you forget to check the operation in one place, or even perhaps forget to write a return in the if clause, the operation proceeds anyhow. You need to write some structure where operations can't be reached without a positive affirmation that it is allowed. I'd bet the code that was broken due to failing to check an error amounted to this in the end. (Edit: Oh, I see you did say that.) And, like I said, this is independent of Go; other than the capabilities-based languages this code can be written in pretty much anything.)
    • tptacek 9 days ago

      I think it's a reasonable observation but it isn't a fair comparative security criteria. The subtext behind error checking critiques is that languages with idiomatic sum type returns avoid authz vulnerabilities, in the same way that memory-safety in Go eliminates UAF vulnerabilities. But authz vulnerabilities are endemic to the mainstream sum type languages, too; they're much more complicated as a bug class than just "am I forced to check return codes before using return values".

      Sum types are one of the few things I miss when switching from other languages back to Go. I like them a lot. But I think they're wildly overstated as a security feature. Sum type languages have external tooling projects to spot authz vulnerabilities!

      • [removed] 9 days ago
        [deleted]
  • randomdata 9 days ago

    > "I forgot to check the error code"

    How is it that people "forget to check errors" but not other types, even though they are all just 1s and 0s? Or, to put it another way, why do programmers forget how to program as soon as they see the word "error"?

    It seems to be a real phenomenon, but I can't make sense of how it can happen. It is not some subtle thing like misspelling a word in a string constant. You are leaving out entire functionality from your application. It is almost on the order of forgetting to add the main function.

    • vacuity 9 days ago

      I would think it's a mix of not being sure exactly what to do on error and not wanting to undergo the effort of writing error logic. You have to switch from "basic skeletal structure of the program" to "cover all bases", which isn't simple. So it's easy to have no or rudimentary error handling, and by the time you want to change it, it's hard to change. Like, "malloc can fail, but it would be a lot easier right now if I assume it won't".

  • arccy 9 days ago

    as if DoS by exception is any better...

    • Smaug123 9 days ago

      Depends on the application! There's a reason we have the concept of "failing closed" vs "failing open": sometimes (very often, in fact) it's correct to shut down under attack, rather than to open up under attack.

      • tptacek 9 days ago

        The subtext of that comment cuts against the argument you're trying to make here: a panic following a missed error check is always fail-closed, but exception recovery is not.

fweimer 9 days ago

One thing to note about data races in Go is that the safe Go subset is only memory-safe if you do not have data races. The original post alludes to that because it mentions the race detector. This situation is different from Java where the expected effect of data races on memory safety is bounded (originally due to the sandbox, now bounded effects are more of QoI aspect). Data races in Java are still bad, and your code may go into infinite loops if you have them (among other things), but they won't turn a long into an object reference.

The good news is that the Go implementation can be changed to handle data races more gracefully, with some additional run-time overhead and some increase in compiler complexity and run-time library complexity, but without language changes. I expect this to happen eventually, once someone manages to get code execution through a data race in a high-profile Go application and publishes the results.

  • tptacek 9 days ago

    These arguments would be more compelling if they came with actual exploitable vulnerabilities --- in shipped code, with real threat models --- demonstrating them, but of course the lived experience of professional programmers is that non-contrived Go memory safety vulnerabilities are so rare as to be practically nonexistent.

tgv 9 days ago

About footguns, I'd like to mention an important one: in Go, it's hard to deserialize data wrongly. It's not like python and typescript where you declare your input data to be one thing, and then receive something else. It's a feature that makes server code, which after all is Go's niche, considerably more reliable.

Safety isn't 0% or 100%, and the more a language offers, the better the result. Go is performant, safe, and fairly easy to read and write. What else do you need (in 99.9% of the cases)?

  • js2 9 days ago

    > It's not like python and typescript where you declare your input data to be one thing, and then receive something else

    In Python that's likely to lead to a runtime TypeError, not so much in TS since at runtime it's JS and JS is weakly typed.

    Besides, Python has Pydantic which everyone should really should be using. :-)

    • tgv 9 days ago

      Only if you use a deserializer that's tied to your classes, and not put everything in a dict. And then only if the data encounters an operation that doesn't accept it. But many operations accept e.g. strings, arrays, ints and floats. Is there even an operation that throws a TypeError when using a float instead of int?

      Pydantic only helps (AFAIK) when you're letting it help, and you actually use the correct type information. It's not difficult to use, but it's optional, and can be faulty.

cyberax 9 days ago

> I'd say Go largely lacks the footguns some other languages have

It does have a couple of its own. Like ((*SomeStruct)(nil)).(SomeInterface) != nil.

And yeah, the error handling is fucked up.

  • jerf 9 days ago

    I was referring specifically to security footguns like having a string eval. While one can construct code in which that is the critical error that led to a security vulnerability, that can be said about any confusing bit of code in any language, and I would not judge that to especially lead to security issues.

    • cyberax 9 days ago

      This actually is a security footgun. In Java or C# you can't get security issues by trying to update a reference from multiple threads, because it's always atomic. In Go you can create type confusion because interface pointer updates are not atomic.

      • tptacek 9 days ago

        Point to a real, exploitable, public vulnerability that exploits this behavior, and then we'll all be talking about the same thing.

      • jerf 9 days ago

        This sets the bar ludicrously low for "security footgun". If this is a "security footgun" then what is string evaluation in a dynamic scripting language, a "security foot-nuke"?

        Granted, there is no sharp line that can be drawn, but given my personal career I'd say I've encountered it personally at least once is a reasonable bar, if not quite excessively low. (tptacek would have to set the bar somewhere else, given his career.) Concurrency issues causing a security issue because of type confusion on an interface in a Go program is not a "every time I crack open a program, oi, this security vulnerability again" like bad HTML escaping or passing things straight to a shell. I mean, "concurrency issues causing type confusion on an interface" is already not something I've ever personally witnessed, let alone it actually being a security issue rather than a difficult-to-trace panic issue.

        And I will reiterate, I already say that any bug can become a security issue in the right context. That doesn't make them all "security footguns".

        • cyberax 9 days ago

          > This sets the bar ludicrously low for "security footgun". If this is a "security footgun" then what is string evaluation in a dynamic scripting language, a "security foot-nuke"?

          Not really. Apart from dangerous serialization formats (e.g. Python's "pickle") it's not at all easy to eval a string in modern scripting languages.

          String evals are also not widely used anymore.