Merge pull request #12901 from porcupineyhairs/goDsn

Go: Add query to detect DSN Injection.
This commit is contained in:
Chris Smowton
2023-05-11 22:45:43 +01:00
committed by GitHub
11 changed files with 264 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
func bad() interface{} {
name := os.Args[1:]
// This is bad. `name` can be something like `test?allowAllFiles=true&` which will allow an attacker to access local files.
dbDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8", "username", "password", "127.0.0.1", 3306, name)
db, _ := sql.Open("mysql", dbDSN)
return db
}

View File

@@ -0,0 +1,12 @@
func good() (interface{}, error) {
name := os.Args[1]
hasBadChar, _ := regexp.MatchString(".*[?].*", name)
if hasBadChar {
return nil, errors.New("Bad input")
}
dbDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8", "username", "password", "127.0.0.1", 3306, name)
db, _ := sql.Open("mysql", dbDSN)
return db, nil
}

View File

@@ -0,0 +1,38 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>If a Data-Source Name (DSN) is built using untrusted user input without proper sanitization,
the system may be vulnerable to DSN injection vulnerabilities.</p>
</overview>
<recommendation>
<p>If user input must be included in a DSN, additional steps should be taken to sanitize
untrusted data, such as checking for special characters included in user input.</p>
</recommendation>
<example>
<p>In the following examples, the code accepts the db name from the user,
which it then uses to build a DSN string.</p>
<p>The following example uses the unsanitized user input directly
in the process of constructing a DSN name.
A malicious user could provide special characters to change the meaning of this string, and
carry out unexpected database operations.</p>
<sample src="DsnBad.go" />
<p>In the following example, the input provided by the user is sanitized before it is included
in the DSN string.
This ensures the meaning of the DSN string cannot be changed by a malicious user.</p>
<sample src="DsnGood.go" />
</example>
<references>
<li>
CVE-2022-3023: <a href="https://nvd.nist.gov/vuln/detail/CVE-2022-3023/">Data Source Name Injection in pingcap/tidb.</a>
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,22 @@
/**
* @name SQL Data-source URI built from user-controlled sources
* @description Building an SQL data-source URI from untrusted sources can allow attacker to compromise security
* @kind path-problem
* @problem.severity error
* @id go/dsn-injection
* @tags security
* experimental
* external/cwe/cwe-134
*/
import go
import DataFlow::PathGraph
import DsnInjectionCustomizations
/** An untrusted flow source taken as a source for the `DsnInjection` taint-flow configuration. */
private class UntrustedFlowAsSource extends Source instanceof UntrustedFlowSource { }
from DsnInjection cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This query depends on a $@.", source.getNode(),
"user-provided value"

View File

@@ -0,0 +1,46 @@
/** Provides a taint-tracking model to reason about Data-Source name injection vulnerabilities. */
import go
import DataFlow::PathGraph
import semmle.go.dataflow.barrierguardutil.RegexpCheck
/** A source for `DsnInjection` taint-flow configuration. */
abstract class Source extends DataFlow::Node { }
/** A taint-tracking configuration to reason about Data Source Name injection vulnerabilities. */
class DsnInjection extends TaintTracking::Configuration {
DsnInjection() { this = "DsnInjection" }
override predicate isSource(DataFlow::Node node) { node instanceof Source }
override predicate isSink(DataFlow::Node node) {
exists(Function f | f.hasQualifiedName("database/sql", "Open") |
node = f.getACall().getArgument(1)
)
}
override predicate isSanitizer(DataFlow::Node node) { node instanceof RegexpCheckBarrier }
}
/** A model of a function which decodes or unmarshals a tainted input, propagating taint from any argument to either the method receiver or return value. */
private class DecodeFunctionModel extends TaintTracking::FunctionModel {
DecodeFunctionModel() {
// This matches any function with a name like `Decode`,`Unmarshal` or `Parse`.
// This is done to allow taints stored in encoded forms, such as in toml or json to flow freely.
this.getName().regexpMatch("(?i).*(parse|decode|unmarshal).*")
}
override predicate hasTaintFlow(FunctionInput input, FunctionOutput output) {
input.isParameter(_) and
(output.isResult(0) or output.isReceiver())
}
}
/** A model of `flag.Parse`, propagating tainted input passed via CLI flags to `Parse`'s result. */
private class FlagSetFunctionModel extends TaintTracking::FunctionModel {
FlagSetFunctionModel() { this.hasQualifiedName("flag", "Parse") }
override predicate hasTaintFlow(FunctionInput input, FunctionOutput output) {
input.isParameter(0) and output.isResult()
}
}

View File

@@ -0,0 +1,24 @@
/**
* @name SQL Data-source URI built from local user-controlled sources
* @description Building an SQL data-source URI from untrusted sources can allow attacker to compromise security
* @kind path-problem
* @problem.severity error
* @id go/dsn-injection-local
* @tags security
* experimental
* external/cwe/cwe-134
*/
import go
import DataFlow::PathGraph
import DsnInjectionCustomizations
/** An argument passed via the command line taken as a source for the `DsnInjection` taint-flow configuration. */
private class OsArgsSource extends Source {
OsArgsSource() { this = any(Variable c | c.hasQualifiedName("os", "Args")).getARead() }
}
from DsnInjection cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This query depends on a $@.", source.getNode(),
"user-provided value"

View File

@@ -0,0 +1,77 @@
package main
import (
"database/sql"
"errors"
"fmt"
"net/http"
"os"
"regexp"
)
func good() (interface{}, error) {
name := os.Args[1]
hasBadChar, _ := regexp.MatchString(".*[?].*", name)
if hasBadChar {
return nil, errors.New("bad input")
}
dbDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8", "username", "password", "127.0.0.1", 3306, name)
db, _ := sql.Open("mysql", dbDSN)
return db, nil
}
func bad() interface{} {
name2 := os.Args[1:]
// This is bad. `name` can be something like `test?allowAllFiles=true&` which will allow an attacker to access local files.
dbDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8", "username", "password", "127.0.0.1", 3306, name2[0])
db, _ := sql.Open("mysql", dbDSN)
return db
}
func good2(w http.ResponseWriter, req *http.Request) (interface{}, error) {
name := req.FormValue("name")
hasBadChar, _ := regexp.MatchString(".*[?].*", name)
if hasBadChar {
return nil, errors.New("bad input")
}
dbDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8", "username", "password", "127.0.0.1", 3306, name)
db, _ := sql.Open("mysql", dbDSN)
return db, nil
}
func bad2(w http.ResponseWriter, req *http.Request) interface{} {
name := req.FormValue("name")
// This is bad. `name` can be something like `test?allowAllFiles=true&` which will allow an attacker to access local files.
dbDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8", "username", "password", "127.0.0.1", 3306, name)
db, _ := sql.Open("mysql", dbDSN)
return db
}
type Config struct {
dsn string
}
func NewConfig() *Config { return &Config{dsn: ""} }
func (Config) Parse([]string) error { return nil }
func RegexFuncModelTest(w http.ResponseWriter, req *http.Request) (interface{}, error) {
cfg := NewConfig()
err := cfg.Parse(os.Args[1:]) // This is bad. `name` can be something like `test?allowAllFiles=true&` which will allow an attacker to access local files.
if err != nil {
return nil, err
}
dbDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8", "username", "password", "127.0.0.1", 3306, cfg.dsn)
db, _ := sql.Open("mysql", dbDSN)
return db, nil
}
func main() {
bad2(nil, nil)
good()
bad()
good2(nil, nil)
}

View File

@@ -0,0 +1,8 @@
edges
| Dsn.go:47:10:47:30 | call to FormValue | Dsn.go:50:29:50:33 | dbDSN |
nodes
| Dsn.go:47:10:47:30 | call to FormValue | semmle.label | call to FormValue |
| Dsn.go:50:29:50:33 | dbDSN | semmle.label | dbDSN |
subpaths
#select
| Dsn.go:50:29:50:33 | dbDSN | Dsn.go:47:10:47:30 | call to FormValue | Dsn.go:50:29:50:33 | dbDSN | This query depends on a $@. | Dsn.go:47:10:47:30 | call to FormValue | user-provided value |

View File

@@ -0,0 +1 @@
experimental/CWE-134/DsnInjection.ql

View File

@@ -0,0 +1,27 @@
edges
| Dsn.go:26:11:26:17 | selection of Args | Dsn.go:29:29:29:33 | dbDSN |
| Dsn.go:62:2:62:4 | definition of cfg [pointer] | Dsn.go:63:9:63:11 | cfg [pointer] |
| Dsn.go:62:2:62:4 | definition of cfg [pointer] | Dsn.go:67:102:67:104 | cfg [pointer] |
| Dsn.go:63:9:63:11 | cfg [pointer] | Dsn.go:63:9:63:11 | implicit dereference |
| Dsn.go:63:9:63:11 | implicit dereference | Dsn.go:62:2:62:4 | definition of cfg [pointer] |
| Dsn.go:63:9:63:11 | implicit dereference | Dsn.go:63:9:63:11 | implicit dereference |
| Dsn.go:63:9:63:11 | implicit dereference | Dsn.go:68:29:68:33 | dbDSN |
| Dsn.go:63:19:63:25 | selection of Args | Dsn.go:63:9:63:11 | implicit dereference |
| Dsn.go:63:19:63:25 | selection of Args | Dsn.go:68:29:68:33 | dbDSN |
| Dsn.go:67:102:67:104 | cfg [pointer] | Dsn.go:67:102:67:104 | implicit dereference |
| Dsn.go:67:102:67:104 | implicit dereference | Dsn.go:63:9:63:11 | implicit dereference |
| Dsn.go:67:102:67:104 | implicit dereference | Dsn.go:68:29:68:33 | dbDSN |
nodes
| Dsn.go:26:11:26:17 | selection of Args | semmle.label | selection of Args |
| Dsn.go:29:29:29:33 | dbDSN | semmle.label | dbDSN |
| Dsn.go:62:2:62:4 | definition of cfg [pointer] | semmle.label | definition of cfg [pointer] |
| Dsn.go:63:9:63:11 | cfg [pointer] | semmle.label | cfg [pointer] |
| Dsn.go:63:9:63:11 | implicit dereference | semmle.label | implicit dereference |
| Dsn.go:63:19:63:25 | selection of Args | semmle.label | selection of Args |
| Dsn.go:67:102:67:104 | cfg [pointer] | semmle.label | cfg [pointer] |
| Dsn.go:67:102:67:104 | implicit dereference | semmle.label | implicit dereference |
| Dsn.go:68:29:68:33 | dbDSN | semmle.label | dbDSN |
subpaths
#select
| Dsn.go:29:29:29:33 | dbDSN | Dsn.go:26:11:26:17 | selection of Args | Dsn.go:29:29:29:33 | dbDSN | This query depends on a $@. | Dsn.go:26:11:26:17 | selection of Args | user-provided value |
| Dsn.go:68:29:68:33 | dbDSN | Dsn.go:63:19:63:25 | selection of Args | Dsn.go:68:29:68:33 | dbDSN | This query depends on a $@. | Dsn.go:63:19:63:25 | selection of Args | user-provided value |

View File

@@ -0,0 +1 @@
experimental/CWE-134/DsnInjectionLocal.ql