Skip to content

Commit df31a0e

Browse files
mustansir14amanfcp
andauthored
Fix JDBC Detector Bugs (#4548)
* add tests * skip verification if host or password is empty * incorporate zach's suggestion * change logs verbosity level to 2 * revert unintentional change --------- Co-authored-by: Amaan Ullah <[email protected]>
1 parent e7b3dbf commit df31a0e

File tree

6 files changed

+385
-13
lines changed

6 files changed

+385
-13
lines changed

pkg/detectors/jdbc/mysql.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,34 @@ func isMySQLErrorDeterminate(err error) bool {
5151
return false
5252
}
5353

54-
func parseMySQL(_ logContext.Context, subname string) (jdbc, error) {
54+
func parseMySQL(ctx logContext.Context, subname string) (jdbc, error) {
5555
// expected form: [subprotocol:]//[user:password@]HOST[/DB][?key=val[&key=val]]
5656
if !strings.HasPrefix(subname, "//") {
5757
return nil, errors.New("expected host to start with //")
5858
}
5959

6060
// need for hostnames that have tcp(host:port) format required by this database driver
6161
cfg, err := mysql.ParseDSN(strings.TrimPrefix(subname, "//"))
62-
if err == nil {
63-
return &mysqlJDBC{
64-
conn: subname[2:],
65-
userPass: cfg.User + ":" + cfg.Passwd,
66-
host: fmt.Sprintf("tcp(%s)", cfg.Addr),
67-
params: "timeout=5s",
68-
}, nil
62+
if err != nil {
63+
// fall back to URI parsing
64+
return parseMySQLURI(ctx, subname)
65+
}
66+
67+
if cfg.Addr == "" || cfg.Passwd == "" {
68+
ctx.Logger().WithName("jdbc").
69+
V(2).
70+
Info("Skipping invalid MySQL URL - no password or host found")
71+
return nil, fmt.Errorf("missing host or password in connection string")
6972
}
73+
return &mysqlJDBC{
74+
conn: subname[2:],
75+
userPass: cfg.User + ":" + cfg.Passwd,
76+
host: fmt.Sprintf("tcp(%s)", cfg.Addr),
77+
params: "timeout=5s",
78+
}, nil
79+
}
80+
81+
func parseMySQLURI(ctx logContext.Context, subname string) (jdbc, error) {
7082

7183
// for standard URI format, which is all i've seen for JDBC
7284
u, err := url.Parse(subname)
@@ -88,11 +100,15 @@ func parseMySQL(_ logContext.Context, subname string) (jdbc, error) {
88100
pass = v
89101
}
90102

91-
userAndPass := user
92-
if pass != "" {
93-
userAndPass = userAndPass + ":" + pass
103+
if u.Host == "" || pass == "" {
104+
ctx.Logger().WithName("jdbc").
105+
V(2).
106+
Info("Skipping invalid MySQL URL - no password or host found")
107+
return nil, fmt.Errorf("missing host or password in connection string")
94108
}
95109

110+
userAndPass := user + ":" + pass
111+
96112
return &mysqlJDBC{
97113
conn: subname[2:],
98114
userPass: userAndPass,

pkg/detectors/jdbc/mysql_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package jdbc
2+
3+
import (
4+
"context"
5+
"strings"
6+
"testing"
7+
8+
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
9+
)
10+
11+
func TestParseMySQLMissingCredentials(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
subname string
15+
shouldBeNil bool
16+
reason string
17+
}{
18+
{
19+
name: "no password - should return nil",
20+
subname: "//examplehost.net:3306/dbname?user=admin",
21+
shouldBeNil: true,
22+
reason: "no password present",
23+
},
24+
{
25+
name: "no password (tcp format) - should return nil",
26+
subname: "//tcp(examplehost.net:3306)/dbname?user=admin",
27+
shouldBeNil: true,
28+
reason: "no password present in tcp format",
29+
},
30+
{
31+
name: "no host - should return nil",
32+
subname: "///dbname?user=admin&password=secret123",
33+
shouldBeNil: true,
34+
reason: "no host present",
35+
},
36+
{
37+
name: "no host and no password - should return nil",
38+
subname: "///dbname",
39+
shouldBeNil: true,
40+
reason: "no host or password present",
41+
},
42+
{
43+
name: "valid with host and password - should succeed",
44+
subname: "//examplehost.net:3306/dbname?user=root&password=secret123",
45+
shouldBeNil: false,
46+
},
47+
{
48+
name: "valid with tcp(host:port) format - should succeed",
49+
subname: "//root:secret123@tcp(examplehost.net:3306)/dbname",
50+
shouldBeNil: false,
51+
},
52+
{
53+
name: "valid with localhost - should succeed",
54+
subname: "//localhost/dbname?user=root&password=secret123",
55+
shouldBeNil: false,
56+
},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
ctx := logContext.AddLogger(context.Background())
62+
j, err := parseMySQL(ctx, tt.subname)
63+
64+
if tt.shouldBeNil {
65+
if j != nil {
66+
t.Errorf("parseMySQL() expected nil (%s), got: %v", tt.reason, j)
67+
}
68+
} else {
69+
if j == nil {
70+
t.Errorf("parseMySQL() returned nil, expected valid connection. err = %v", err)
71+
}
72+
if err != nil {
73+
t.Errorf("parseMySQL() unexpected error = %v", err)
74+
}
75+
}
76+
})
77+
}
78+
}
79+
80+
func TestParseMySQLUsernameRecognition(t *testing.T) {
81+
tests := []struct {
82+
name string
83+
subname string
84+
wantUsername string
85+
}{
86+
{
87+
name: "user parameter specified",
88+
subname: "//localhost:3306/dbname?user=myuser&password=mypass",
89+
wantUsername: "myuser",
90+
},
91+
{
92+
name: "no user specified - default root",
93+
subname: "//localhost:3306/dbname?password=mypass",
94+
wantUsername: "root",
95+
},
96+
{
97+
name: "user specified (tcp format)",
98+
subname: "//myuser:secret123@tcp(localhost:3306)/dbname",
99+
wantUsername: "myuser",
100+
},
101+
}
102+
103+
for _, tt := range tests {
104+
t.Run(tt.name, func(t *testing.T) {
105+
ctx := logContext.AddLogger(context.Background())
106+
j, err := parseMySQL(ctx, tt.subname)
107+
if err != nil {
108+
t.Fatalf("parseMySQL() error = %v", err)
109+
}
110+
111+
mysqlConn := j.(*mysqlJDBC)
112+
if !strings.Contains(mysqlConn.userPass, tt.wantUsername) {
113+
t.Errorf("Connection string does not contain expected username '%s'\nGot: %s\nExpected: %s",
114+
tt.wantUsername, mysqlConn.userPass, tt.wantUsername)
115+
}
116+
})
117+
}
118+
}

pkg/detectors/jdbc/postgres.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func joinKeyValues(m map[string]string, sep string) string {
5959
return strings.Join(data, sep)
6060
}
6161

62-
func parsePostgres(_ logContext.Context, subname string) (jdbc, error) {
62+
func parsePostgres(ctx logContext.Context, subname string) (jdbc, error) {
6363
// expected form: [subprotocol:]//[user:password@]HOST[/DB][?key=val[&key=val]]
6464

6565
if !strings.HasPrefix(subname, "//") {
@@ -107,6 +107,13 @@ func parsePostgres(_ logContext.Context, subname string) (jdbc, error) {
107107
params["password"] = v
108108
}
109109

110+
if params["host"] == "" || params["password"] == "" {
111+
ctx.Logger().WithName("jdbc").
112+
V(2).
113+
Info("Skipping invalid Postgres URL - no password or host found")
114+
return nil, fmt.Errorf("missing host or password in connection string")
115+
}
116+
110117
return &postgresJDBC{subname[2:], params}, nil
111118
}
112119

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package jdbc
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
8+
)
9+
10+
func TestParsePostgresMissingCredentials(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
subname string
14+
shouldBeNil bool
15+
reason string
16+
}{
17+
{
18+
name: "no password - should return nil (nothing to verify)",
19+
subname: "//examplehost.net:5432/dbname?user=admin",
20+
shouldBeNil: true,
21+
reason: "no password present",
22+
},
23+
{
24+
name: "no host - should return nil (invalid connection)",
25+
subname: "///dbname?user=admin&password=secret123",
26+
shouldBeNil: true,
27+
reason: "no host present",
28+
},
29+
{
30+
name: "no host and no password - should return nil",
31+
subname: "///dbname",
32+
shouldBeNil: true,
33+
reason: "no host or password present",
34+
},
35+
{
36+
name: "valid with host and password - should succeed",
37+
subname: "//examplehost.net:5432/dbname?user=admin&password=secret123",
38+
shouldBeNil: false,
39+
},
40+
{
41+
name: "valid with localhost and password - should succeed",
42+
subname: "//localhost/dbname?user=postgres&password=secret123",
43+
shouldBeNil: false,
44+
},
45+
}
46+
47+
for _, tt := range tests {
48+
t.Run(tt.name, func(t *testing.T) {
49+
ctx := logContext.AddLogger(context.Background())
50+
j, err := parsePostgres(ctx, tt.subname)
51+
52+
if tt.shouldBeNil {
53+
if j != nil {
54+
t.Errorf("parsePostgres() expected nil (%s), got: %v", tt.reason, j)
55+
}
56+
} else {
57+
if j == nil {
58+
t.Errorf("parsePostgres() returned nil, expected valid connection. err = %v", err)
59+
}
60+
if err != nil {
61+
t.Errorf("parsePostgres() unexpected error = %v", err)
62+
}
63+
}
64+
})
65+
}
66+
}
67+
68+
func TestParsePostgresUsernameRecognition(t *testing.T) {
69+
tests := []struct {
70+
name string
71+
subname string
72+
wantUsername string
73+
}{
74+
{
75+
name: "user parameter specified",
76+
subname: "//localhost:5432/dbname?user=myuser&password=mypass",
77+
wantUsername: "myuser",
78+
},
79+
{
80+
name: "user and password specified",
81+
subname: "//myuser:mypassword@localhost:5432/dbname",
82+
wantUsername: "myuser",
83+
},
84+
}
85+
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
ctx := logContext.AddLogger(context.Background())
89+
j, err := parsePostgres(ctx, tt.subname)
90+
if err != nil {
91+
t.Fatalf("parsePostgres() error = %v", err)
92+
}
93+
94+
pgConn := j.(*postgresJDBC)
95+
if pgConn.params["user"] != tt.wantUsername {
96+
t.Errorf("expected username '%s', got '%s'", tt.wantUsername, pgConn.params["user"])
97+
}
98+
})
99+
}
100+
}

pkg/detectors/jdbc/sqlserver.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func parseSqlServer(ctx logContext.Context, subname string) (jdbc, error) {
4242
conn := strings.TrimPrefix(subname, "//")
4343

4444
port := "1433"
45+
user := "sa"
4546
var password, host string
4647

4748
for i, param := range strings.Split(conn, ";") {
@@ -66,10 +67,20 @@ func parseSqlServer(ctx logContext.Context, subname string) (jdbc, error) {
6667
host = value
6768
case "port":
6869
port = value
70+
case "user", "uid", "user id":
71+
user = value
72+
6973
}
7074
}
7175

72-
urlStr := fmt.Sprintf("sqlserver://sa:%s@%s:%s?database=master&connection+timeout=5", password, host, port)
76+
if password == "" || host == "" {
77+
ctx.Logger().WithName("jdbc").
78+
V(2).
79+
Info("Skipping invalid SQL Server URL - no password or host found")
80+
return nil, fmt.Errorf("missing host or password in connection string")
81+
}
82+
83+
urlStr := fmt.Sprintf("sqlserver://%s:%s@%s:%s?database=master&connection+timeout=5", user, password, host, port)
7384
jdbcUrl, err := url.Parse(urlStr)
7485
if err != nil {
7586
ctx.Logger().WithName("jdbc").

0 commit comments

Comments
 (0)