-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Go: Update the QHelp for go/command-injection
.
#16510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
QHelp previews: go/ql/src/Security/CWE-078/CommandInjection.qhelpCommand built from user-controlled sourcesIf a system command invocation is built from user-provided data without sufficient sanitization, a malicious user may be able to run commands to exfiltrate data or compromise the system. RecommendationWhenever possible, use hard-coded string literals for commands and avoid shell string interpreters like If given arguments as a single string, avoid simply splitting the string on whitespace. Arguments may contain quoted whitespace, causing them to split into multiple arguments. If this is not possible, sanitize user input to avoid characters like spaces and various kinds of quotes that can alter the meaning of the command. ExampleIn the following example, assume the function package main
import (
"net/http"
"os/exec"
)
func handler(req *http.Request) {
imageName := req.URL.Query()["imageName"][0]
outputPath := "/tmp/output.svg"
cmd := exec.Command("sh", "-c", fmt.Sprintf("imagetool %s > %s", imageName, outputPath))
cmd.Run()
// ...
} The handler extracts the image file name from the request and uses the image name to construct a shell command that is executed using It's better to avoid shell commands by using the package main
import (
"log"
"net/http"
"os"
"os/exec"
)
func handler(req *http.Request) {
imageName := req.URL.Query()["imageName"][0]
outputPath := "/tmp/output.svg"
// Create the output file
outfile, err := os.Create(outputPath)
if err != nil {
log.Fatal(err)
}
defer outfile.Close()
// Prepare the command
cmd := exec.Command("imagetool", imageName)
// Set the output to our file
cmd.Stdout = outfile
cmd.Run()
} Alternatively, a regular expression can be used to ensure that the image name is safe to use in a shell command: package main
import (
"log"
"net/http"
"os/exec"
"regexp"
)
func handler(req *http.Request) {
imageName := req.URL.Query()["imageName"][0]
outputPath := "/tmp/output.svg"
// Validate the imageName with a regular expression
validImageName := regexp.MustCompile(`^[a-zA-Z0-9_\-\.]+$`)
if !validImageName.MatchString(imageName) {
log.Fatal("Invalid image name")
return
}
cmd := exec.Command("sh", "-c", fmt.Sprintf("imagetool %s > %s", imageName, outputPath))
cmd.Run()
} Some commands, like To mitigate this risk, either add a package main
import (
"log"
"net/http"
"os/exec"
"strings"
)
func handler(req *http.Request) {
repoURL := req.URL.Query()["repoURL"][0]
outputPath := "/tmp/repo"
// Sanitize the repoURL to ensure it does not start with "--"
if strings.HasPrefix(repoURL, "--") {
log.Fatal("Invalid repository URL")
} else {
cmd := exec.Command("git", "clone", repoURL, outputPath)
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}
// Or: add "--" to ensure that the repoURL is not interpreted as a flag
cmd := exec.Command("git", "clone", "--", repoURL, outputPath)
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
} References
go/ql/src/Security/CWE-078/StoredCommand.qhelpCommand built from stored dataIf a system command invocation is built from stored data without sufficient sanitization, and that data is stored from a user input, a malicious user may be able to run commands to exfiltrate data or compromise the system. RecommendationIf possible, use hard-coded string literals to specify the command to run. Instead of interpreting stored input directly as command names, examine the input and then choose among hard-coded string literals. If this is not possible, then add sanitization code to verify that the user input is safe before using it. ExampleIn the following example, the function package main
import (
"database/sql"
"os/exec"
)
var db *sql.DB
func run(query string) {
rows, _ := db.Query(query)
var cmdName string
rows.Scan(&cmdName)
cmd := exec.Command(cmdName)
cmd.Run()
} The function extracts the name of a system command from the database query, and then runs it without any further checks, which can cause a command-injection vulnerability. A possible solution is to ensure that commands are checked against a whitelist: package main
import (
"database/sql"
"os/exec"
)
var db *sql.DB
func run(query string) {
rows, _ := db.Query(query)
var cmdName string
rows.Scan(&cmdName)
if cmdName == "mybinary1" || cmdName == "mybinary2" {
cmd := exec.Command(cmdName)
}
cmd.Run()
} References
|
@erik-krogh I see that the formatting CI check failed and you fixed it. I worked on it a while ago to make it print out the files that needed to be reformatted, which it hadn't been doing. Did it work for you? Was it easy to see what had failed and what you needed to do? |
Yes, that was helpful 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally this looks good. @atorralba should probably do a quick review as well, since I don't have much app sec experience.
@@ -45,4 +45,11 @@ module CommandInjection { | |||
|
|||
override predicate doubleDashIsSanitizing() { exec.doubleDashIsSanitizing() } | |||
} | |||
|
|||
import semmle.go.dataflow.barrierguardutil.RegexpCheck |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We normally put imports at the top of the module (unless we're importing a module that has just been defined), so for consistency please move it up to line 14.
// Sanitize the repoURL to ensure it does not start with "--" | ||
if strings.HasPrefix(repoURL, "--") { | ||
log.Fatal("Invalid repository URL") | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need a return
here, since log.Fatal
will always exit the program. But I guess there's no harm in having it.
The func handler(req *http.Request) {
cmd := req.URL.Query()["cmd"][0]
if strings.HasPrefix(cmd, "--") {
log.Fatal("Invalid cmd")
}
cmd := exec.Command(cmd)
cmd.Run()
} If that's the case, this seems dangerous and prone to FNs. The regex one is a tradeoff: there will be regexes that correctly sanitize the problem, and ones that not. With this sanitizer, we're prioritizing having less FPs in exchange of risking more FNs, which I'm not sure is what we want right now. Ideally, we'd be able to analyze the regex to ensure that it either:
Of course, this can get very complicated very quickly, and is probably overkill, so I'll leave it up to you if you want to merge as is, improve the sanitizer, or remove it entirely. Maybe a decent alternative would be accepting less complex solutions that check the presence of whitespaces, like |
Yes, that example would spuriously be marked as safe. I don't think it's a problem that the sanitizer just sanitizes everything, because the So yes, FNs are possible, but I think those would be weird.
We could do something nice, if Go had an implementation of the shared NFA libraries.
Checking just for whitespace is not a valid sanitizer for anything related to command-injection? Can you give me an example of what you mean? |
If this is not possible, sanitize user input to avoid characters like spaces and | ||
various kinds of quotes that can alter the meaning of the command. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking just for whitespace is not a valid sanitizer for anything related to command-injection? Can you give me an example of what you mean?
Yep, I know it's not enough, I was just referring to this part of the QHelp ⬆️ in case you wanted to do something more specific and simpler without using regex. But it wouldn't still be perfect of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to mention which characters are dangerous, just to provide some context.
But deny-list based sanitizers for command injection are generally a bad idea, which is why I didn't do it that way in the example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, I won't challenge your decisions. As long as we're collectively aware of the shortcomings of these sanitizers, and decide they're worth it, then I have nothing against this PR. :)
It's definitely a thing we do elsewhere in the Go library to consider any regexp check a sanitizer without confirming that they've actually checked for the right thing. So I think it's fine to do here too. |
Also fixed some FPs that appeared in the fix-examples in the new QHelp.
My friend likes this new QHelp.
An evaluation was very uneventful.
No change in results or performance.