Skip to content

Commit ae2cc37

Browse files
author
Porcupiney Hairs
committed
Golang : Add Query To Detect PAM Authorization Bugs
1 parent a661a0c commit ae2cc37

File tree

11 files changed

+313
-0
lines changed

11 files changed

+313
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
func bad() error {
2+
t, err := pam.StartFunc("", "username", func(s pam.Style, msg string) (string, error) {
3+
switch s {
4+
case pam.PromptEchoOff:
5+
return string(pass), nil
6+
}
7+
return "", fmt.Errorf("unsupported message style")
8+
})
9+
if err != nil {
10+
return nil, err
11+
}
12+
13+
if err := t.Authenticate(0); err != nil {
14+
return nil, fmt.Errorf("Authenticate: %w", err)
15+
}
16+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
Using only a call to
6+
<code>pam.Authenticate</code>
7+
to check the validity of a login can lead to authorization bypass vulnerabilities.
8+
</p>
9+
<p>
10+
A
11+
<code>pam.Authenticate</code>
12+
only verifies the credentials of a user. It does not check if a user has an
13+
appropriate authorization to actually login. This means a user with an expired
14+
login or a password can still access the system.
15+
</p>
16+
17+
</overview>
18+
19+
<recommendation>
20+
<p>
21+
A call to
22+
<code>pam.Authenticate</code>
23+
should be followed by a call to
24+
<code>pam.AcctMgmt</code>
25+
to check if a user is allowed to login.
26+
</p>
27+
</recommendation>
28+
29+
<example>
30+
<p>
31+
In the following example, the code only checks the credentials of a user. Hence,
32+
in this case, a user with expired credentials can still login. This can be
33+
verified by creating a new user account, expiring it with
34+
<code>chage -E0 `username` </code>
35+
and then trying to log in.
36+
</p>
37+
<sample src="PamAuthBad.go" />
38+
39+
<p>
40+
This can be avoided by calling
41+
<code>pam.AcctMgmt</code>
42+
call to verify access as has been done in the snippet shown below.
43+
</p>
44+
<sample src="PamAuthGood.go" />
45+
</example>
46+
47+
<references>
48+
<li>
49+
Man-Page:
50+
<a href="https://man7.org/linux/man-pages/man3/pam_acct_mgmt.3.html">pam_acct_mgmt</a>
51+
</li>
52+
</references>
53+
</qhelp>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @name PAM authorization bypass due to incorrect usage
3+
* @description Not using `pam.AcctMgmt` after `pam.Authenticate` to check the validity of a login can lead to authorization bypass.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id go/unreachable-statement
7+
* @tags maintainability
8+
* correctness
9+
* external/cwe/cwe-561
10+
* external/cwe/cwe-285
11+
* @precision very-high
12+
*/
13+
14+
import go
15+
16+
predicate isInTestFile(Expr r) {
17+
r.getFile().getAbsolutePath().matches("%test%") and
18+
not r.getFile().getAbsolutePath().matches("%/ql/test/%")
19+
}
20+
21+
class PamAuthenticate extends Method {
22+
PamAuthenticate() {
23+
this.hasQualifiedName("github.com/msteinert/pam", "Transaction", "Authenticate")
24+
}
25+
}
26+
27+
class PamAcctMgmt extends Method {
28+
PamAcctMgmt() { this.hasQualifiedName("github.com/msteinert/pam", "Transaction", "AcctMgmt") }
29+
}
30+
31+
class PamTransaction extends StructType {
32+
PamTransaction() { this.hasQualifiedName("github.com/msteinert/pam", "Transaction") }
33+
}
34+
35+
class PamStartFunc extends Function {
36+
PamStartFunc() { this.hasQualifiedName("github.com/msteinert/pam", ["StartFunc", "Start"]) }
37+
}
38+
39+
class PamAuthBypassConfiguration extends TaintTracking::Configuration {
40+
PamAuthBypassConfiguration() { this = "PAM auth bypass" }
41+
42+
override predicate isSource(DataFlow::Node source) {
43+
exists(PamStartFunc p | p.getACall().getResult(0) = source)
44+
}
45+
46+
override predicate isSink(DataFlow::Node sink) {
47+
exists(PamAcctMgmt p | p.getACall().getReceiver() = sink)
48+
}
49+
}
50+
51+
class PamAuthBypassConfig extends TaintTracking::Configuration {
52+
PamAuthBypassConfig() { this = "PAM auth bypass2" }
53+
54+
override predicate isSource(DataFlow::Node source) {
55+
exists(PamStartFunc p | p.getACall().getResult(0) = source)
56+
}
57+
58+
override predicate isSink(DataFlow::Node sink) {
59+
exists(PamAuthenticate p | p.getACall().getReceiver() = sink)
60+
}
61+
}
62+
63+
from
64+
PamAuthBypassConfiguration config, PamAuthBypassConfig config2, DataFlow::Node source,
65+
DataFlow::Node sink
66+
where
67+
not isInTestFile(source.asExpr()) and
68+
(config2.hasFlow(source, sink) and not config.hasFlow(source, _))
69+
select source, "This Pam transaction may not be secure."
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* @name PAM authorization bypass due to incorrect usage
3+
* @description Not using `pam.AcctMgmt` after `pam.Authenticate` to check the validity of a login can lead to authorization bypass.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id go/unreachable-statement
7+
* @tags maintainability
8+
* correctness
9+
* external/cwe/cwe-561
10+
* external/cwe/cwe-285
11+
* @precision very-high
12+
*/
13+
14+
import go
15+
16+
predicate isInTestFile(Expr r) {
17+
r.getFile().getAbsolutePath().matches("%test%") and
18+
not r.getFile().getAbsolutePath().matches("%/ql/test/%")
19+
}
20+
21+
class PamAuthenticate extends Method {
22+
PamAuthenticate() {
23+
this.hasQualifiedName("github.com/msteinert/pam", "Transaction", "Authenticate")
24+
}
25+
}
26+
27+
class PamAcctMgmt extends Method {
28+
PamAcctMgmt() { this.hasQualifiedName("github.com/msteinert/pam", "Transaction", "AcctMgmt") }
29+
}
30+
31+
class PamTransaction extends StructType {
32+
PamTransaction() { this.hasQualifiedName("github.com/msteinert/pam", "Transaction") }
33+
}
34+
35+
class PamStartFunc extends Function {
36+
PamStartFunc() { this.hasQualifiedName("github.com/msteinert/pam", ["StartFunc", "Start"]) }
37+
}
38+
39+
from
40+
DataFlow::Node source, DataFlow::Node sink, PamAuthenticate auth, PamStartFunc start,
41+
PamAcctMgmt actmgt
42+
where
43+
not isInTestFile(source.asExpr()) and
44+
(
45+
start.getACall().getResult(0) = source and
46+
sink = auth.getACall().getReceiver() and
47+
DataFlow::localFlow(source, sink) and
48+
not DataFlow::localFlow(source, actmgt.getACall().getReceiver())
49+
)
50+
select source, "This Pam transaction may not check authorization correctly."
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
func good() error {
2+
t, err := pam.StartFunc("", "username", func(s pam.Style, msg string) (string, error) {
3+
switch s {
4+
case pam.PromptEchoOff:
5+
return string(pass), nil
6+
}
7+
return "", fmt.Errorf("unsupported message style")
8+
})
9+
if err != nil {
10+
return nil, err
11+
}
12+
13+
if err := t.Authenticate(0); err != nil {
14+
return nil, fmt.Errorf("Authenticate: %w", err)
15+
}
16+
if err := t.AcctMgmt(0); err != nil {
17+
return nil, fmt.Errorf("AcctMgmt: %w", err)
18+
}
19+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
| main.go:10:2:12:3 | ... := ...[0] | This Pam transaction may not be secure. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/CWE-285/PamAuthBypass.ql
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module main
2+
3+
go 1.18
4+
5+
require github.com/msteinert/pam v1.0.0
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package main
2+
3+
//go:generate depstubber -vendor github.com/msteinert/pam Style,Transaction StartFunc
4+
5+
import (
6+
"github.com/msteinert/pam"
7+
)
8+
9+
func bad() error {
10+
t, _ := pam.StartFunc("", "", func(s pam.Style, msg string) (string, error) {
11+
return "", nil
12+
})
13+
return t.Authenticate(0)
14+
15+
}
16+
17+
func good() error {
18+
t, err := pam.StartFunc("", "", func(s pam.Style, msg string) (string, error) {
19+
return "", nil
20+
})
21+
err = t.Authenticate(0)
22+
if err != nil {
23+
return err
24+
}
25+
return t.AcctMgmt(0)
26+
}
27+
28+
func main() {}

go/ql/test/experimental/CWE-285/vendor/github.com/msteinert/pam/stub.go

Lines changed: 68 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)