Simple implementation for limiting shell commands. - #23956
Conversation
d539a62 to
6addf79
Compare
|
There are still some features I might add but I'm going to open this up for review to get some feedback. Is this something anyone else wants? |
Chain segments are all evaluated before any are executed, to prevent partial state issues. Syntax validation is not performed. The shell handles this.
|
The problem is that this looks like a security feature but cannot be one as long as the string still goes to sh -c: a newline already bypasses the segment check entirely, and whitelisting a program name does not constrain its arguments. On Linux the only boundary that actually holds is one enforced by the kernel, so the sandbox has to be a locked down container rather than a filter, and #26062 already gives you the clean way to get there: declare an MCP stdio server whose command runs inside that container, and llama.cpp then only ever passes structured JSON params to a process whose argv you fixed yourself, so no model controlled string ever reaches a shell outside the container. Inside it, a tool is free to call sh -c on model input, because the container is the boundary and the parsing no longer buys you anything. |
|
This was a stupid oversight on my part. That bug is fixed now. Thanks, @ServeurpersoCom! 🙂 I agree that MCP over stdio is generally better in a lot of ways, but I think this is also useful. With that said, I wrote this before MCP over stdio was added and I haven't actually tried MCP. I will some day soon. I tried to highlight the risks in the |
|
Even if it can be handy in the moment, we won't be able to merge this. The blocker isn't the individual bugs, it's the framing: a feature that can be taken for a security option without actually being one leads to too much downstream misuse, people will rely on it as a boundary and build on that false assumption. And this is the part that makes it worse than a static bug: a model will naturally and easily start working around the filter exactly when you least expect it, not out of malice but because routing around an obstacle to complete the task is what they do, so the whitelist gets bypassed in normal operation, not just under a crafted attack. That's a line we can't cross regardless of how the implementation evolves. For the record, a non-exhaustive sample of the failure modes, this is a bottomless pit and patching each one just moves the next one into view:
|
|
If you don't want to merge it, that's ok. I agree that it's not perfect. Nothing is-- especially in security. I feel like this is reasonably safe, barring any bugs, and it is certainly a lot safer than simply using the With regards to your list of concerns:
|
|
Quick answer to your direct question: POSIX, single segment "nice id". id isn't a second segment to whitelist, it's an argument to nice, and nice execs it. Same for env sh -c id, timeout 5 id, setsid id, xargs id, find . -exec id ;. argv[0] is whitelisted, the argument is the payload. That's the whole point: whitelisting the program name can't constrain a program whose job is to run another one. On ls -l vs ls -la: it does match. The code memcmp's s.length() bytes and never checks the following char, so ls -l accepts ls -la /etc/shadow. There's no "delim must follow" in the logic. You're right about ^, drop that one, it's not a clean bypass against an exact argv[0] match. And this isn't about removing exec_shell_command. It's about not shipping a filter under a security label it can't hold. The tool as raw is honestly unsafe and says so; a filter that looks safe but isn't is worse, because people build on the false assumption. |
|
Yeah, what @ServeurpersoCom said, we've discussed this already: we are explicitly marking the tool as unsafe to let the people know it is such. What we really don't want is shipping an imperfect guard as something that "hardens" the solution, because it's the worst of both worlds: it doesn't really secure it against attacks (since an attacker will try every vector), while it gives uses a false sense of security. |
|
My POV is: if you can show us how other open-source harness do this, then let's investigate to see how they did better. Otherwise, my honest take: from security perspective, you cannot do much with whitelisting commands, as you pointed out:
A lot of commands can run other commands, so mostly of time user will end up with a too restricted set of whilelist that turns the exec_shell tool to useless. The LLM may also waste token to find the useful commands that are non-blocked, or try to abuse whitelisted commands to escape. Sounds familiar? From system perspective, it's better to just do a proper sandbox if you really care about security. I already planned this when I reworked the tools, and already had a draft locally that run tools directly inside a docker container. I'll need a bit of time to test it, but will push it soon. |
|
We all agree. For most setups, running llama.cpp itself inside a container makes shell exec safe by default: the boundary is already there and the tool inherits it for free, no filter needed. On macOS, App Store apps are likewise sandboxed into per-app containers by default, same idea at the OS level. Either way the kernel does the work, and we're not going to reimplement rbash inside llama.cpp lol. And restricting a model to a command subset tends to hurt its performance quite a bit anyway. When you actually want to narrow what it can do, the better path is to declare a tool list rather than gate a shell: a plain bash or powershell script is enough (there's a minimal example in the stdio PR). That's a whitelist instead of a blacklist, and any security mistake lives in a user script, not in the project's C++. |
Oh, yes. I thought you were saying something else with that point.
You're right, I mis-read it when I replied. I must have dropped that logic in my iterations on the last commit. I'll push a fix in a couple hours.
👍
I agree with the principle but it seems a bit excessive in this case. I guess the issue is that we have no way of signalling the danger effectively enough to manage the risk. Users who want this (in my opinion critical) functionality will have to add it themself or build some other fork. That's a major nuisance but I guess it's an effective way of making sure users know what they are doing.
I absolutely agree if the hardening is unreliable but I feel like there's a point where it is safe enough and the security concerns are detached enough from the project to be unrelated. I understand your decision though. I mostly made this for myself anyway. 👍
If you're open to merging this if it's reliable, I can look into that a bit. It seems like the consensus is that this is not something you want though. If you all change your mind, just let me know and I will do what I can.
So far, I've had an easy time using this now that pipes are supported and the LLM can query the programs it has access to. It's not perfect but I've had no real issues since those features were added. Of course, I'm sure it depends on the model and the prompt.
Ok. Personally, I don't want to containerise programs on my desktop or personal server but I would likely use some sort of container on a production server. They feel like different use cases to me, but I know a lot of people do prefer containers in general.
Ok, I will close the P.R. for now. Let me know if you all change your mind. I'm happy to do what I can to make it safe and useful for others. |
take my advice here as a grant of salt, but this seems to be a false sense of security. you may want to containerize when it runs on personal env because it would be super bad if the agent accidentally mess up with your home dir (or worse, rm -rf it ; and this is exactly why whitelisting command is bad: if the model try to a production env should already run inside a container anyway. at worse, it deletes the database or the mounted data dir, not nuking the host's home dir or root dir |
|
Thank you. I do understand the risk, but I'm comfortable enough with this for now. I use system permissions for access control and none of the commands I have whitelisted can modify files or system state. They're mostly coreutils, so pretty unlikely to have intrinsic vulnerabilities or updates that introduce unexpected features. I can also use sandboxing. My aversion to containers specifically is just that they add a lot of complexity when really all I would want is sandboxing. The main benefit I see in containers is orchestration for scaling. Hopefully that makes sense. If my system does get wiped somehow, I do also keep backups. It would be an inconvenience but I'm comfortable taking that risk. 🙂 |
Overview
Allows for limiting the shell commands available as tools.
Additional information
This is still lacking some features that might be nice to have (see below), but it works reasonably well.
Features that should probably be added:
Features that could be added:
Limitations:
Warnings:
Requirements