mirror of
https://github.com/github/codeql.git
synced 2026-04-24 00:05:14 +02:00
Merge pull request #12901 from porcupineyhairs/goDsn
Go: Add query to detect DSN Injection.
This commit is contained in:
8
go/ql/src/experimental/CWE-134/DsnBad.go
Normal file
8
go/ql/src/experimental/CWE-134/DsnBad.go
Normal 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
|
||||
}
|
||||
12
go/ql/src/experimental/CWE-134/DsnGood.go
Normal file
12
go/ql/src/experimental/CWE-134/DsnGood.go
Normal 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
|
||||
}
|
||||
38
go/ql/src/experimental/CWE-134/DsnInjection.qhelp
Normal file
38
go/ql/src/experimental/CWE-134/DsnInjection.qhelp
Normal 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>
|
||||
22
go/ql/src/experimental/CWE-134/DsnInjection.ql
Normal file
22
go/ql/src/experimental/CWE-134/DsnInjection.ql
Normal 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"
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
24
go/ql/src/experimental/CWE-134/DsnInjectionLocal.ql
Normal file
24
go/ql/src/experimental/CWE-134/DsnInjectionLocal.ql
Normal 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"
|
||||
77
go/ql/test/experimental/CWE-134/Dsn.go
Normal file
77
go/ql/test/experimental/CWE-134/Dsn.go
Normal 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)
|
||||
}
|
||||
8
go/ql/test/experimental/CWE-134/DsnInjection.expected
Normal file
8
go/ql/test/experimental/CWE-134/DsnInjection.expected
Normal 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 |
|
||||
1
go/ql/test/experimental/CWE-134/DsnInjection.qlref
Normal file
1
go/ql/test/experimental/CWE-134/DsnInjection.qlref
Normal file
@@ -0,0 +1 @@
|
||||
experimental/CWE-134/DsnInjection.ql
|
||||
27
go/ql/test/experimental/CWE-134/DsnInjectionLocal.expected
Normal file
27
go/ql/test/experimental/CWE-134/DsnInjectionLocal.expected
Normal 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 |
|
||||
1
go/ql/test/experimental/CWE-134/DsnInjectionLocal.qlref
Normal file
1
go/ql/test/experimental/CWE-134/DsnInjectionLocal.qlref
Normal file
@@ -0,0 +1 @@
|
||||
experimental/CWE-134/DsnInjectionLocal.ql
|
||||
Reference in New Issue
Block a user