Eliminating OS Command Injection with Unikernels

CWE-78 - OS Command Injection was ranked number three as the top ten KEV weaknesses last year. It's cousin, CWE-77 was number 10. There is a lot of advice out there out about preventing it but why not just eliminate it as CISA has called for?

A lot of folks pay attention to the CISA KEV because if you work in the government you are required to patch during a certain time frame. There's another list though. On the top 25 most dangerous software weaknesses it is ranked 7 and 13 respectively. There is a difference between these two but it is often quite misattributed and one of them almost always leads to the second one anyways but we'll get to that - that is why we simply group them together.

Last year we saw CVE-2024-3400, a 10.0 CVSS Score affect Palo Alto's (a very large cybersecurity company) PAN-OS software - resulting in an unauthenticated RCE.

Similarily, Ivanti Connect Secure (another cybersecurity company) received a 9.1 score for CVE-2024-21887.

Even this year we've already seen a handful of cves winding up in KEV that are because of command injection including CVE-2025-8876 in N-Able scoring a 8.8 and CVE-2025-10035 for Fortra GoAnywhere getting the highest 10.0 score.

What can make these vulnerabilities really dangerous is when it's command injection without authentication or running as root - then you are really screwed. Like pokemon if you catch all three of these - you are guaranteed to get news headlines. ... and unfortunately everyone that isn't a security nerd winds up getting owned in the worst possible way a year or so later.

OS command injection is still a highly pervasive threat and unikernels are the only architecture that completely obviate them. A lot of advice for preventing os command injection basically boils down to:

  • validating and sanitizing user input
  • use 'safer' functions like execvp vs exec

That's great advice but the best defense is to simply not call os commands at all and unikernels enforce that.

OS Command Injection Examples

In java you might see runtime.exec or processbuilder to get a directory output:

Bad Java


import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Hello {
    public static void main(String[] args) {
        try {
            String command = "ls";

            Process process = Runtime.getRuntime().exec(command);
            System.out.println("cmd:" + command);

            System.out.println(process.getInputStream());
            int exitCode = process.waitFor();

           BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

            String line;
            System.out.println("out:");
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }

            System.out.println("exit:" + exitCode);

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Howver, in java instead of calling those functions we can call something like listFiles instead:

Good Java


import java.io.File;

public class ListFiles {

    public static void main(String[] args) {
        String directoryPath = ".";
        File directory = new File(directoryPath);

        if (directory.exists() && directory.isDirectory()) {
            File[] files = directory.listFiles();

            if (files != null) {
                System.out.println("Files in " + directoryPath + ":");
                for (File file : files) {
                    if (file.isFile()) {
                        System.out.println(file.getName());
                    }
                }
            }
        } else {
            System.out.println("err");
        }
    }
}

For the same operation In PHP you might see calls to exec:

Bad PHP


 <?php
  $command = "ls";
  $output = [];
  $return_var;
  exec($command, $output, $return_var);
  echo "Output:\n";
  print_r($output);
  echo "status: " . $return_var . "\n";
?>

We can replace that by using scandir instead:

Good PHP


<?php
$dir = '.';
$files = scandir($dir);

foreach ($files as $file) {
    if ($file != '.' && $file != '..') {
        echo $file . "
"; } } ?>

In javascript we might see yet another call to 'exec':

Bad Javascript


const { exec, spawn } = require('child_process');

exec('ls', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

We can replace that with readdir here:

Good Javascript


const fs = require('fs');
const path = require('path');

const directoryPath = '.';

fs.readdir(directoryPath, (err, files) => {
  if (err) {
    console.error("Error reading directory:", err);
    return;
  }
  files.forEach(file => {
    console.log(file);
  });
});

In go you might see exec.Command:

Bad Go


package main

import (
    "fmt"
    "os/exec"
)

func main() {
    cmd := exec.Command("ls", ".")
    output, err := cmd.Output()
    if err != nil {
        fmt.Printf("Error executing command: %v\n", err)
        return
    }
    fmt.Printf("Command output:\n%s\n", output)
}

Again, we can use ReadDir here instead:

Good Go


package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    dirPath := "."

    entries, err := os.ReadDir(dirPath)
    if err != nil {
        log.Fatal(err)
    }

    for _, entry := range entries {
        fmt.Printf("%s\n", entry.Name())
    }
}

In languages like ruby os command injection can be as simple as a simple backtick:

f = '/tmp; touch /tmp/owned;'
`ls #{f}`

Code injection vs Command injection

There is a similar but different weakness called code injection.

The difference between code injection (CWE-94) vs command injection (CWE-77, CWE-78) is pretty simple. Can you just run code in something like an interpreter or parser vs actually running commands on the end system?

For example if we look at this javascript code:

> x = 10
> mycode = "let x =" + x + "; let y = 20; console.log(x + y);";
> eval(mycode);

We could potentially change x to run other arbitrary javascript code if we have access to it.

> x = "2; console.log('test')"
> eval(mycode)
test
22

For this example since we're clearly adding two numbers together you might wish to sanitize the input to ensure it is indeed a number and not random javascript code.

How to Replace Potential OS Command Injection Calls

First off - ensure you are deploying as unikernels as that eliminates this on the spot. However, to get there you might need to replace some of these calls.

The easiest thing to do is look for native functions that you can replace with as shown in the prior code examples.

What about the times when there are no native functions you could use? What about linking to a library? For performance reasons the scripting interpreters have long used FFI and other means to call into c libraries for things like video encoding, image processing, or even much more mundane things such as parsing json.

What if no native function exists and no native library exists? This is when you call the pros and have them create something for you. Yes, NanoVMs can help with this.

If you ever see yourself reaching for os.exec or spawn or something else you should reconsider using a native library or linking to a library instead of spawning a process.

System Command Injection vs Command Injection?

The difference between command injection (CWE-77) versus os command injection (CWE-78) might be subtle but a lot of stuff that is marked CWE-77 should probably be marked CWE-78. To further complicate things the remaining chunk that is in CWE-77 should also probably be classified as argument injection (CWE-88).

An example of the proper use of CWE-77 could be found in CVE-2022-1509. Assuming the second line is the vulnerable part of a shell script and the first line is the input that we are abusing - technically we're not invoking any sort of system commands but we can still inject commands to sed:

after='/;s/.*/hacked/;wowned.txt
#'
box:~/ss$ echo abcbdbe | sed "s/b/$after/g"
hacked

(Note: That this wouldn't work on a unikernel system to begin with though as we don't run shells or shell scripts.)

OS Command Injection Is a Very Common End Goal of an Attacker

Keep in mind that unikernels not only thwart this specific weakness but they also perform exploit payload mitigation for attacks that need the ability to run other programs and typically attackers don't want to run just one other program but many other ones. So even though something might not be categorized as an 'os command injection' you should consider what the actual end goal of an attacker is.

Many vulnerabilities can get categorized differently too. Even though they are not explicitly called out as such. Take for example CVE-2025-61882 - something that starts out with a SSRF, performs an improper path traversal and winds up doing an XSLT transformation eventually allows the attacker to... wait for it.. inject their own commands into the template.

But wait there's more..

In practice you simply can't just simply replace all these insecure (and slow) calls to things like os.exec and continue to run your code in containers or linux in general. Why not? Imagine you are using something like javascript and you have a form that allows users to upload files. If you aren't careful with sanitizing input, and the user can call a new javascript file they just uploaded, they can just upload their own code and issue their own calls to exec.

This can also happen in your ci/cd pipeline and you might not even know it is happening. The recent Shai Hulud malware and other NPM disasters were good examples of this.

This practice is a lot more common than you might think in real life. Many many exploits are a part of an exploit chain where they take advantage of many issues, just like the CVE-2025-61882 issue above.

Also, you might think compiled code is immune to these RFI or supply chain attacks but that couldn't be further from the truth. This is exactly how attackers worked before things became... easier.

This is why it is important to not only remove this insecure code from your codebase but also deploy unikernels. You need a more hardened runtime rather thany relying on scanners.

Really eliminating os command injection goes a lot further than preventing these calls in your code. Deploying unikernels by definition removes the capability of running other programs. Why does this matter? When we make a call to os.exec or the like - it might say "command" but each "command" is yet another program and unikernels explicitly prohibit this. Unlike a seccomp profile or something that might 'forbid' it from happening, unikernels don't even have the support to run it in the first place.

Have an existing app you would like to unikernelize but know you are going to be running into these issues? Need help? Reach out and NanoVMs can help you migrate quickly and easily.

Stop Deploying 50 Year Old Systems

Introducing the future cloud.

Ready for the future cloud?

Ready for the revolution in operating systems we've all been waiting for?

Schedule a Demo