1. 28
  1. 16

    I think this needs to be solved at the OS level, not the language level.

    It’s a problem for every language – packages you download can write to any file or make network connections. It might be a little worse in NPM because of the culture, but the problem is pervasive.

    Personally I’m interested in the direction of lightweight containers that behave like executables, and that have composable dependencies. Developing in Docker-like containers can work but there are a bunch of downsides to be mitigated.

    1. 11

      It’s a problem for every language – packages you download can write to any file or make network connections.

      No, it’s only a problem for capability-unsafe languages. By analogy, if I said “packages you download can corrupt arbitrary memory,” a reasonable objection might be that it’s only a problem for memory-unsafe languages.

      1. 6

        So you mean the languages that approximately 100% of software is written in?

        I’m familiar with various efforts / ideas around making Python, Go, and JavaScript capability safe, going back ~20 years.

        Language-based capabilities have an inherent O(N^2) interoperability problem. The problem is that security is systems issue, and systems are written in more than one language.

        A capability in JS means nothing to Go – you need some kind of bridge, and N^2 bridges in general. The bridges themselves will also have usability and security issues.

        It seems like you’re confusing research and practice … if not I would like to read about the deployed systems you’re thinking about.

        1. 7

          The analogy to memory-unsafety is rather deep. There was a time when all software was memory-unsafe, all languages were memory-unsafe, and garbage collection was considered too expensive to ever deploy in production.

          Your objection to transporting capabilities between languages can also be analogized, although it comes in two cases, depending on whether the communicating processes are colocated. When the processes are next to each other, then they can share memory through various shared-memory schemes; analogously, file descriptors are capabilities which can be communicated from process to process. For processes which are remote to each other, they cannot truly share memory or capabilities, but they can capture snapshots of memory and they can export capabilities with cryptography.

          The flavor of E I worked on, Monte, supports loading untrusted packages safely. The code is freely available; I built a generic computation server and a Warner-style spellserver. But, it needs an explanation in order to be usable. I can tell you exactly which language features make this possible.

          First, in any flavor of E, loading code does not execute it. Instead, code objects are created which represent the loaded code. In Monte, compilation to bytecode preserves the structure of code objects, so that loading a package is like creating a literal AST value. Evaluation is always relative to a specified environment. Even the “safe scope” of standard objects is not available to evaluators by default.

          Second, Monte implements E-style auditors, and in particular, it is possible for an object to prove itself transitively immutable to anybody who asks. Modules are annotated by the compiler so that the module and its exports are proven transitively immutable at runtime.

          Note that I used AMP with JSON payloads to transport capabilities, but Capn Proto RPC would be a more serious choice for production deployments, with explicit support for representing and sending messages to capabilities.

          1. 2

            I’m not sure the n^2 is really a problem if we’re talking about most practical software. In almost all cases it looks like (for example) JS with a native library for “very specific things” like the event loop or some file/service interface. At that point you can use a single bridge, or just still gain lots from capabilities by treating anything past FFI as a black box. (Or even isolate it separately through seccomp/selinux context)

            We’ve got lots to gain even if we ignore the bridges.

          2. 5

            Yeah, this can’t ONLY be solved at the OS level. I should be able to build a web application where an npm package gives me a better Button component but isn’t allowed to mine bitcoin and access the network. But some other npm package in my application might be an ORM that talks to my database and needs network access.

            Like Corbin says, your language needs to be able to define and enforce capabilities. The OS can only really help with enforcement.

            1. 1

              I’d argue that there’s little value in a button component as buttons as is are fine… and this is a part of the JS eco problem—gotta have a package/component to solve every thing. I came into a project using some React video player, and the thing injected a CDN script at runtime without my permission.

              1. 3

                Sorry you didn’t like my completely arbitrary example.

                Thinking everything is so simple that you never need packages or abstractions is a really tired meme. A Button component could typed in a way that prevents bad (eg poor accessibility) usage while still being a <button> under the covers. Is that so bad?

                1. 1

                  I just gave you a concrete example of something that literally could have been a <video> tag and instead injects arbitrary scripts. I spend 90% of my time around the NPM ecosystem and no, it’s not meme, and does lead to problems like auditing and these side effects as mentioned about giving too many permissions to untrusted packages to npm install is-array.

                  I still don’t like this example either. Abstracting over an element often obsures just as much as it helps. What is inaccessible about the built-in button? I see people putting onclick on random elements without role=button and no hover/focus states all the time because they don’t understand that <button> is already accessible.

                  1. 4

                    Icon-only buttons need a label or a screen reader can’t announce them properly and you can’t have a tooltip when mousing over them. That’s why we have tools like Lighthouse that can audit your site and tell you when you’ve screwed that up. <button>s are not always accessible just by existing.

                    That said, unless your thesis is that all packages are inherently bad, this is an extremely off-topic argument. If my hypothetical package is not to your liking, imagine another.

            2. 4

              Given the behaviours of almost all standard libraries and standard package managers that are popular these days, it’s pretty clear that the incentives of OSS don’t line up with this being done at the language level, so yes I’d say there’s a very strong case to make that it should be done at the OS level.

              1. 3

                At which level would you say that Nix operates? While it is leaky, Nix and similar package managers attempt to treat package references as capabilities, and also attempt to prevent untrusted packages from exploiting builders by limiting their ambient authority.

                1. 1

                  I’m not really qualified to answer, I know of Nix and generally feel it seems more sensible than most package managers, but I haven’t used it.

            3. 4

              I think this needs to be solved at the OS level, not the language level.

              This is possible only if your language uses OS abstractions for isolating modules. This is not common and in something like NodeJS would require a separate V8 instance per module, which would be prohibitive (imagine if the function to trim whitespace needed 300 MiB of RAM for a JS JIT, all by itself).

              For this to work well, you need cooperation between a capability-secure set of OS abstraction (which, currently, means that your choice of operating systems is narrowed down to FreeBSD or Fuchsia) and capability-secure language abstractions. Within a type-safe language, you can rely on type safety to preserve the capabilities. Across languages you need to use OS abstractions. Ideally, you want a Multics-style library model, where a single process has isolated memory regions for each library.

              Hmm, it’s almost as if that’s what I’ve spent the last decade building…

              1. 2

                What’s the best intro to that work?

                I read papers on Capsicum ~10 years ago but otherwise haven’t kept up with it. When I look at the page now it seems like it’s partially deployed ? I’d be interested in some kind of retrospective.

                https://www.cl.cam.ac.uk/research/security/capsicum/

                Or maybe the problems with it are what led to the desire for hardware and language support?

                Either way I think this supports my point … Adding capabilities just to node.js is of limited use – it’s a systemic issue and all layers of the stack are affected.


                One reason I’m interested in shell is because it does use OS abstractions for isolating modules :) From my POV an entire node.js program is a “module”. There were a lot of motivations for this, but one is that at Google, the security reviews for services would frequently recommend or even require that certain libraries be run in separate processes. For example I recall some huge PDF and or internationalization libraries handling user input. They were simply too risky to link into processes running at a high privilege level.

                1. 2

                  This blog, which was posted on lobste.rs is probably the best overview of the things that I’m working on. I regard Capsicum as basically a solved problem at this point - it’s a retrofit of ideas from ‘80s capability systems to POSIX and it does that well. If you don’t need POSIX then you can design APIs better (and Zircon / Fuchsia does). Extending that into something that can work at a library level and isolate C/C++/assembly code that is used in a typesafe language is the more difficult part of the problem.

                  1. 2

                    I remember reading about Capsicum when it was brand new and being pretty excited about it. Unfortunately, AFAIK it still hasn’t really made any headway beyond FreeBSD and experimental changes to some programs. I read somewhere (but might be wrong) that the Chromium team didn’t manage to keep Capsicum support maintained, probably because it doesn’t exist on Linux.

                2. 2

                  It might be a little worse in NPM because of the culture, but the problem is pervasive.

                  I think it’s perceived as “worse” in NPM because it’s so widely used, and tends to lean on the side of smaller packages with more focused goals, therefore you need more of them in order to complete an implementation. This is in contrast to Python packages like NumPy or SciPy, which try to do “everything related to math” or “everything related to science” and are relatively large libraries in comparison. RubyGems has this problem to a lesser extent because it’s a middle-ground between huge Python packages and tiny JS packages. But in reality, all languages are vulnerable to this kind of attack, so I agree with you that this might be better implemented on the OS level.

                  But good luck getting all the OSes to agree on a standard!

                  Personally I’m interested in the direction of lightweight containers that behave like executables, and that have composable dependencies.

                  Can you elaborate on how this might help? I’m a big fan of containerized applications but mostly because it prevents polluting global namespaces and causing problems that are difficult to diagnose. You still need some kind of system for defining what capabilities a program has, and it can’t just be a “yes/no” thing like macOS has. Ideally, we’d take some inspiration from OAuth2 and implement some kind of scope-based access that allows the program to tell the user exactly the kinds of permissions it needs. This would stop a lot of the more annoying supply-chain attacks, and would require a much more targeted approach, which I think would alleviate the problem for most people since they are not targeted.

                  1. 2

                    Right, the culture of fine-grained libraries is a difference in severity in JS, but not a difference in principle.

                    I think for dev tools, Linux is the main system that needs to be changed / upgraded. For example git started out as Linux-only and spread to OS X and Windows. And same with Docker – when you run Docker on OS X or Windows, you’re also running Linux.

                    node.js is used mainly for servers and CLIs, which mostly run on Linux, especially in production. So I think a solution there will go a long way.


                    The way I’m thinking of it is that we need coarse-grained principle of least privilege in two main contexts:

                    1. At build time. For example, the TypeScript compiler is a huge JS blob, which I believe 99% of people run unsandboxed.

                    When I mean “containers that behave like executables”, I mean that you should be able to run:

                    tsc-sandboxed myinput myoutput
                    

                    And it will only be given the capability to read myinput and write myoutput. It should not be able to write ~/.bashrc! So this is a form of coarse-grained security that I believe a shell can help with.

                    Note that https://oilshell.org is also growing a declarative part that is supposed to be better “YAML”. It will evaluate to JSON, so you could use it for the JSON spec in Docker/OCI containers, package.json, etc.

                    1. At runtime. Say you are building a JavaScript server like https://stackeditpro.com or something. You might invoke it as:

                      stackedit-sandboxed –port 8080 –data-root ~/stackedit

                    It should be able to listen to that port, but not any others. And it should be able to read and write data under its root dir, but no others. That could be implemented with a file system namespace.

                    Again, it shouldn’t be able to write to ~/.bashrc, /etc/passwd, or exfiltrate data via UDP or DNS and so forth.

                    Again I believe maybe 90% of people run such servers unsandboxed in their dev environments. Docker is good for production but there’s a bit too much friction for development. Some people do it though.


                    So that is the general idea I’m interested in. Capabilities should be explicit and specified with flags or configuration. The benefit of having it in a shell-like language is that it composes. Systems are composed out of many parts and you use language to compose them.

                    I also think this is not very far from standard practice. It doesn’t require drastic rewriting or rethinking of how we develop systems. You don’t need to rewrite your software in a different language or use a different operating system.

                    There are a lot of other details, always happy to chat more, e.g. on https://oilshell.zulipchat.com/

                    1. 2

                      So you basically want Deno’s permissions model applied to the entire OS?

                      https://deno.land/manual/getting_started/permissions

                      Not a bad idea. It takes a little getting used to when you first start out with Deno, but the idea that you can whitelist files to read, subprocesses to execute, and network endpoints to call and ensure that the program cannot do anything past that. It’s funny because Ryan Dahl referred to the way Deno handled security as “Web-style sandboxing” in the initial talk he gave introducing the project, since he was very much influenced by the security model of browsers when building it.

                      Other than out-of-the-box TypeScript compliation (which is an absolute bear on NodeJS given all the different configuration possibilities…), I think the security model is the most compelling feature of Deno, and I wish it was available in any language I chose to use.

                      1. 2

                        It would be default-deny without ambient authority, so yes it looks similar. The web started out as mostly default-deny, although browser vendors have been poking some questionable holes in it … Still better than app stores though.

                        FWIW I’m keeping track of related threads here: https://oilshell.zulipchat.com/#narrow/stream/266575-blog-ideas/topic/Dumpling.20container.20format

                        I don’t know much about Deno, but subresource integrity also came up, and that is related to the “composable dependencies” I mentioned. They should be pinned by hashes / content. Except they’re not just JavaScript files; they’re git-like subtrees of runnable code.

                        I think OS X has been moving to a stricter default-deny model as well. There are a lot of UI problems there but what I’m envisioning is more for server-side distributed computing.

                3. 2

                  This is a really nice proposal for a solution, hidden behind a title that made me roll my eyes and think “oh god, not another one of these posts”.