Skip to content

Commit f74dff5

Browse files
authored
Merge pull request #187 from github/hardcoded-credentials
Add rb/hardcoded-credentials query
2 parents 82fbc03 + 8839d4c commit f74dff5

6 files changed

Lines changed: 344 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Including unencrypted hard-coded inbound or outbound authentication credentials within source code
9+
or configuration files is dangerous because the credentials may be easily discovered.
10+
</p>
11+
<p>
12+
Source or configuration files containing hard-coded credentials may be visible to an attacker. For
13+
example, the source code may be open source, or it may be leaked or accidentally revealed.
14+
</p>
15+
<p>
16+
For inbound authentication, hard-coded credentials may allow unauthorized access to the system. This
17+
is particularly problematic if the credential is hard-coded in the source code, because it cannot be
18+
disabled easily. For outbound authentication, the hard-coded credentials may provide an attacker with
19+
privileged information or unauthorized access to some other system.
20+
</p>
21+
22+
</overview>
23+
<recommendation>
24+
25+
<p>
26+
Remove hard-coded credentials, such as user names, passwords and certificates, from source code,
27+
placing them in configuration files or other data stores if necessary. If possible, store
28+
configuration files including credential data separately from the source code, in a secure location
29+
with restricted access.
30+
</p>
31+
32+
<p>
33+
For outbound authentication details, consider encrypting the credentials or the enclosing data
34+
stores or configuration files, and using permissions to restrict access.
35+
</p>
36+
37+
<p>
38+
For inbound authentication details, consider hashing passwords using standard library functions
39+
where possible. For example, <code>OpenSSL::KDF.pbkdf2_hmac</code>.
40+
</p>
41+
42+
</recommendation>
43+
<example>
44+
45+
<p>
46+
The following examples shows different types of inbound and outbound authentication.
47+
</p>
48+
49+
<p>
50+
In the first case, <code>RackAppBad</code>, we accept a password from a remote user, and compare
51+
it against a plaintext string literal. If an attacker acquires the source code they can observe
52+
the password, and can log in to the system. Furthermore, if such an intrusion was discovered, the
53+
application would need to be rewritten and redeployed in order to change the password.
54+
</p>
55+
56+
<p>
57+
In the second case, <code>RackAppGood</code>, the password is compared to a hashed and salted
58+
password stored in a configuration file, using <code>OpenSSL::KDF.pbkdf2_hmac</code>.
59+
In this case, access to the source code or the assembly would not reveal the password to an
60+
attacker. Even access to the configuration file containing the password hash and salt would be of
61+
little value to an attacker, as it is usually extremely difficult to reverse engineer the password
62+
from the hash and salt. In a real application care should be taken to make the string comparison
63+
of the hashed input against the hashed password take close to constant time, as this will make
64+
timing attacks more difficult.
65+
</p>
66+
67+
<sample src="HardcodedCredentials.rb" />
68+
69+
</example>
70+
<references>
71+
72+
<li>
73+
OWASP:
74+
<a href="https://www.owasp.org/index.php/Use_of_hard-coded_password">XSS
75+
Use of hard-coded password</a>.
76+
</li>
77+
78+
</references>
79+
</qhelp>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* @name Hard-coded credentials
3+
* @description Credentials are hard coded in the source code of the application.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @precision high
7+
* @id rb/hardcoded-credentials
8+
* @tags security
9+
* external/cwe/cwe-259
10+
* external/cwe/cwe-321
11+
* external/cwe/cwe-798
12+
*/
13+
14+
import ruby
15+
import codeql_ruby.DataFlow
16+
import DataFlow::PathGraph
17+
private import codeql_ruby.controlflow.CfgNodes
18+
19+
bindingset[char, fraction]
20+
predicate fewer_characters_than(StringLiteral str, string char, float fraction) {
21+
exists(string text, int chars |
22+
text = str.getValueText() and
23+
chars = count(int i | text.charAt(i) = char)
24+
|
25+
/* Allow one character */
26+
chars = 1 or
27+
chars < text.length() * fraction
28+
)
29+
}
30+
31+
predicate possible_reflective_name(string name) {
32+
// TODO: implement this?
33+
none()
34+
}
35+
36+
int char_count(StringLiteral str) { result = count(string c | c = str.getValueText().charAt(_)) }
37+
38+
predicate capitalized_word(StringLiteral str) { str.getValueText().regexpMatch("[A-Z][a-z]+") }
39+
40+
predicate format_string(StringLiteral str) { str.getValueText().matches("%{%}%") }
41+
42+
predicate maybeCredential(Expr e) {
43+
/* A string that is not too short and unlikely to be text or an identifier. */
44+
exists(StringLiteral str | str = e |
45+
/* At least 10 characters */
46+
str.getValueText().length() > 9 and
47+
/* Not too much whitespace */
48+
fewer_characters_than(str, " ", 0.05) and
49+
/* or underscores */
50+
fewer_characters_than(str, "_", 0.2) and
51+
/* Not too repetitive */
52+
exists(int chars | chars = char_count(str) |
53+
chars > 15 or
54+
chars * 3 > str.getValueText().length() * 2
55+
) and
56+
not possible_reflective_name(str.getValueText()) and
57+
not capitalized_word(str) and
58+
not format_string(str)
59+
)
60+
or
61+
/* Or, an integer with over 32 bits */
62+
exists(IntegerLiteral lit | lit = e |
63+
not exists(lit.getValue()) and
64+
/* Not a set of flags or round number */
65+
not lit.getValueText().matches("%00%")
66+
)
67+
}
68+
69+
class HardcodedValueSource extends DataFlow::Node {
70+
HardcodedValueSource() { maybeCredential(this.asExpr().getExpr()) }
71+
}
72+
73+
/**
74+
* Gets a regular expression for matching names of locations (variables, parameters, keys) that
75+
* indicate the value being held is a credential.
76+
*/
77+
private string getACredentialRegex() {
78+
result = "(?i).*pass(wd|word|code|phrase)(?!.*question).*" or
79+
result = "(?i).*(puid|username|userid).*" or
80+
result = "(?i).*(cert)(?!.*(format|name)).*"
81+
}
82+
83+
bindingset[name]
84+
private predicate maybeCredentialName(string name) {
85+
name.regexpMatch(getACredentialRegex()) and
86+
not name.suffix(name.length() - 4) = "file"
87+
}
88+
89+
// Positional parameter
90+
private DataFlow::Node credentialParameter() {
91+
exists(Method m, NamedParameter p, int idx |
92+
result.asParameter() = p and
93+
p = m.getParameter(idx) and
94+
maybeCredentialName(p.getName())
95+
)
96+
}
97+
98+
// Keyword argument
99+
private Expr credentialKeywordArgument() {
100+
exists(MethodCall mc, string argKey |
101+
result = mc.getKeywordArgument(argKey) and
102+
maybeCredentialName(argKey)
103+
)
104+
}
105+
106+
// An equality check against a credential value
107+
private Expr credentialComparison() {
108+
exists(EqualityOperation op, VariableReadAccess vra |
109+
maybeCredentialName(vra.getVariable().getName()) and
110+
(
111+
op.getLeftOperand() = result and
112+
op.getRightOperand() = vra
113+
or
114+
op.getLeftOperand() = vra and op.getRightOperand() = result
115+
)
116+
)
117+
}
118+
119+
private predicate isCredentialSink(DataFlow::Node node) {
120+
node = credentialParameter()
121+
or
122+
node.asExpr().getExpr() = credentialKeywordArgument()
123+
or
124+
node.asExpr().getExpr() = credentialComparison()
125+
}
126+
127+
class CredentialSink extends DataFlow::Node {
128+
CredentialSink() { isCredentialSink(this) }
129+
}
130+
131+
class HardcodedCredentialsConfiguration extends DataFlow::Configuration {
132+
HardcodedCredentialsConfiguration() { this = "HardcodedCredentialsConfiguration" }
133+
134+
override predicate isSource(DataFlow::Node source) { source instanceof HardcodedValueSource }
135+
136+
override predicate isSink(DataFlow::Node sink) { sink instanceof CredentialSink }
137+
138+
override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
139+
exists(ExprNodes::BinaryOperationCfgNode binop |
140+
(
141+
binop.getLeftOperand() = node1.asExpr() or
142+
binop.getRightOperand() = node1.asExpr()
143+
) and
144+
binop = node2.asExpr() and
145+
// string concatenation
146+
binop.getExpr() instanceof AddExpr
147+
)
148+
}
149+
}
150+
151+
from DataFlow::PathNode source, DataFlow::PathNode sink, HardcodedCredentialsConfiguration conf
152+
where conf.hasFlowPath(source, sink)
153+
select source.getNode(), source, sink, "Use of $@.", source.getNode(), "hardcoded credentials"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
require 'rack'
2+
require 'yaml'
3+
require 'openssl'
4+
5+
class RackAppBad
6+
def call(env)
7+
req = Rack::Request.new(env)
8+
password = req.params['password']
9+
10+
# BAD: Inbound authentication made by comparison to string literal
11+
if password == 'myPa55word'
12+
[200, {'Content-type' => 'text/plain'}, ['OK']]
13+
else
14+
[403, {'Content-type' => 'text/plain'}, ['Permission denied']]
15+
end
16+
end
17+
end
18+
19+
class RackAppGood
20+
def call(env)
21+
req = Rack::Request.new(env)
22+
password = req.params['password']
23+
24+
config_file = YAML.load_file('config.yml')
25+
hashed_password = config_file['hashed_password']
26+
salt = [config_file['salt']].pack('H*')
27+
28+
#GOOD: Inbound authentication made by comparing to a hash password from a config file.
29+
hash = OpenSSL::Digest::SHA256.new
30+
dk = OpenSSL::KDF.pbkdf2_hmac(
31+
password, salt: salt, hash: hash, iterations: 100_000, length: hash.digest_length
32+
)
33+
hashed_input = dk.unpack('H*').first
34+
if hashed_password == hashed_input
35+
[200, {'Content-type' => 'text/plain'}, ['OK']]
36+
else
37+
[403, {'Content-type' => 'text/plain'}, ['Permission denied']]
38+
end
39+
end
40+
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
edges
2+
| HardcodedCredentials.rb:12:19:12:64 | "4NQX/CqB5Ae98zFUmwj1DMpF7azsh..." : | HardcodedCredentials.rb:1:23:1:30 | password |
3+
| HardcodedCredentials.rb:18:19:18:72 | ... + ... : | HardcodedCredentials.rb:1:23:1:30 | password |
4+
| HardcodedCredentials.rb:18:27:18:72 | "ogH6qSYWGdbR/2WOGYa7eZ/tObL+G..." : | HardcodedCredentials.rb:18:19:18:72 | ... + ... : |
5+
| HardcodedCredentials.rb:20:11:20:76 | "3jOe7sXKX6Tx52qHWUVqh2t9LNsE+..." : | HardcodedCredentials.rb:23:19:23:20 | pw : |
6+
| HardcodedCredentials.rb:21:12:21:37 | "4fQuzXef4f2yow8KWvIJTA==" : | HardcodedCredentials.rb:23:19:23:20 | pw : |
7+
| HardcodedCredentials.rb:23:19:23:20 | pw : | HardcodedCredentials.rb:1:23:1:30 | password |
8+
| HardcodedCredentials.rb:38:40:38:85 | "kdW/xVhiv6y1fQQNevDpUaq+2rfPK..." : | HardcodedCredentials.rb:31:18:31:23 | passwd |
9+
nodes
10+
| HardcodedCredentials.rb:1:23:1:30 | password | semmle.label | password |
11+
| HardcodedCredentials.rb:4:20:4:65 | "xwjVWdfzfRlbcgKkbSfG/xSrUeHYq..." | semmle.label | "xwjVWdfzfRlbcgKkbSfG/xSrUeHYq..." |
12+
| HardcodedCredentials.rb:8:30:8:75 | "X6BLgRWSAtAWG/GaHS+WGGW2K7zZF..." | semmle.label | "X6BLgRWSAtAWG/GaHS+WGGW2K7zZF..." |
13+
| HardcodedCredentials.rb:12:19:12:64 | "4NQX/CqB5Ae98zFUmwj1DMpF7azsh..." : | semmle.label | "4NQX/CqB5Ae98zFUmwj1DMpF7azsh..." : |
14+
| HardcodedCredentials.rb:15:30:15:75 | "WLC17dLQ9P8YlQvqm77qplOMm5pd1..." | semmle.label | "WLC17dLQ9P8YlQvqm77qplOMm5pd1..." |
15+
| HardcodedCredentials.rb:18:19:18:72 | ... + ... : | semmle.label | ... + ... : |
16+
| HardcodedCredentials.rb:18:27:18:72 | "ogH6qSYWGdbR/2WOGYa7eZ/tObL+G..." : | semmle.label | "ogH6qSYWGdbR/2WOGYa7eZ/tObL+G..." : |
17+
| HardcodedCredentials.rb:20:11:20:76 | "3jOe7sXKX6Tx52qHWUVqh2t9LNsE+..." : | semmle.label | "3jOe7sXKX6Tx52qHWUVqh2t9LNsE+..." : |
18+
| HardcodedCredentials.rb:21:12:21:37 | "4fQuzXef4f2yow8KWvIJTA==" : | semmle.label | "4fQuzXef4f2yow8KWvIJTA==" : |
19+
| HardcodedCredentials.rb:23:19:23:20 | pw : | semmle.label | pw : |
20+
| HardcodedCredentials.rb:31:18:31:23 | passwd | semmle.label | passwd |
21+
| HardcodedCredentials.rb:38:40:38:85 | "kdW/xVhiv6y1fQQNevDpUaq+2rfPK..." : | semmle.label | "kdW/xVhiv6y1fQQNevDpUaq+2rfPK..." : |
22+
#select
23+
| HardcodedCredentials.rb:4:20:4:65 | "xwjVWdfzfRlbcgKkbSfG/xSrUeHYq..." | HardcodedCredentials.rb:4:20:4:65 | "xwjVWdfzfRlbcgKkbSfG/xSrUeHYq..." | HardcodedCredentials.rb:4:20:4:65 | "xwjVWdfzfRlbcgKkbSfG/xSrUeHYq..." | Use of $@. | HardcodedCredentials.rb:4:20:4:65 | "xwjVWdfzfRlbcgKkbSfG/xSrUeHYq..." | hardcoded credentials |
24+
| HardcodedCredentials.rb:8:30:8:75 | "X6BLgRWSAtAWG/GaHS+WGGW2K7zZF..." | HardcodedCredentials.rb:8:30:8:75 | "X6BLgRWSAtAWG/GaHS+WGGW2K7zZF..." | HardcodedCredentials.rb:8:30:8:75 | "X6BLgRWSAtAWG/GaHS+WGGW2K7zZF..." | Use of $@. | HardcodedCredentials.rb:8:30:8:75 | "X6BLgRWSAtAWG/GaHS+WGGW2K7zZF..." | hardcoded credentials |
25+
| HardcodedCredentials.rb:12:19:12:64 | "4NQX/CqB5Ae98zFUmwj1DMpF7azsh..." | HardcodedCredentials.rb:12:19:12:64 | "4NQX/CqB5Ae98zFUmwj1DMpF7azsh..." : | HardcodedCredentials.rb:1:23:1:30 | password | Use of $@. | HardcodedCredentials.rb:12:19:12:64 | "4NQX/CqB5Ae98zFUmwj1DMpF7azsh..." | hardcoded credentials |
26+
| HardcodedCredentials.rb:15:30:15:75 | "WLC17dLQ9P8YlQvqm77qplOMm5pd1..." | HardcodedCredentials.rb:15:30:15:75 | "WLC17dLQ9P8YlQvqm77qplOMm5pd1..." | HardcodedCredentials.rb:15:30:15:75 | "WLC17dLQ9P8YlQvqm77qplOMm5pd1..." | Use of $@. | HardcodedCredentials.rb:15:30:15:75 | "WLC17dLQ9P8YlQvqm77qplOMm5pd1..." | hardcoded credentials |
27+
| HardcodedCredentials.rb:18:27:18:72 | "ogH6qSYWGdbR/2WOGYa7eZ/tObL+G..." | HardcodedCredentials.rb:18:27:18:72 | "ogH6qSYWGdbR/2WOGYa7eZ/tObL+G..." : | HardcodedCredentials.rb:1:23:1:30 | password | Use of $@. | HardcodedCredentials.rb:18:27:18:72 | "ogH6qSYWGdbR/2WOGYa7eZ/tObL+G..." | hardcoded credentials |
28+
| HardcodedCredentials.rb:20:11:20:76 | "3jOe7sXKX6Tx52qHWUVqh2t9LNsE+..." | HardcodedCredentials.rb:20:11:20:76 | "3jOe7sXKX6Tx52qHWUVqh2t9LNsE+..." : | HardcodedCredentials.rb:1:23:1:30 | password | Use of $@. | HardcodedCredentials.rb:20:11:20:76 | "3jOe7sXKX6Tx52qHWUVqh2t9LNsE+..." | hardcoded credentials |
29+
| HardcodedCredentials.rb:21:12:21:37 | "4fQuzXef4f2yow8KWvIJTA==" | HardcodedCredentials.rb:21:12:21:37 | "4fQuzXef4f2yow8KWvIJTA==" : | HardcodedCredentials.rb:1:23:1:30 | password | Use of $@. | HardcodedCredentials.rb:21:12:21:37 | "4fQuzXef4f2yow8KWvIJTA==" | hardcoded credentials |
30+
| HardcodedCredentials.rb:38:40:38:85 | "kdW/xVhiv6y1fQQNevDpUaq+2rfPK..." | HardcodedCredentials.rb:38:40:38:85 | "kdW/xVhiv6y1fQQNevDpUaq+2rfPK..." : | HardcodedCredentials.rb:31:18:31:23 | passwd | Use of $@. | HardcodedCredentials.rb:38:40:38:85 | "kdW/xVhiv6y1fQQNevDpUaq+2rfPK..." | hardcoded credentials |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
queries/security/cwe-798/HardcodedCredentials.ql
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
def authenticate(uid, password, cert: nil)
2+
if cert != nil then
3+
# comparison with hardcoded credential
4+
return cert == "xwjVWdfzfRlbcgKkbSfG/xSrUeHYqxPgz9WKN3Yow1o="
5+
end
6+
7+
# comparison with hardcoded credential
8+
uid == 123 and password == "X6BLgRWSAtAWG/GaHS+WGGW2K7zZFTAjJ54fGSudHJk="
9+
end
10+
11+
# call with hardcoded credential as argument
12+
authenticate(123, "4NQX/CqB5Ae98zFUmwj1DMpF7azshxSvb0Jo4gIFmIQ=")
13+
14+
# call with hardcoded credential as argument
15+
authenticate(456, nil, cert: "WLC17dLQ9P8YlQvqm77qplOMm5pd1q25Q2onWqu78JI=")
16+
17+
# concatenation involving literal
18+
authenticate(789, "pw:" + "ogH6qSYWGdbR/2WOGYa7eZ/tObL+GtqDPx6q37BTTRQ=")
19+
20+
pw_left = "3jOe7sXKX6Tx52qHWUVqh2t9LNsE+ZXFj2qw6asRARTV2deAXFKkMTVOoaFYom1Q"
21+
pw_right = "4fQuzXef4f2yow8KWvIJTA=="
22+
pw = pw_left + pw_right
23+
authenticate(999, pw)
24+
25+
passwd = gets.chomp
26+
# call with hardcoded credential-like value, but not to a potential credential sink (should not be flagged)
27+
authenticate("gowLsSGfPbh/ZS60k+LQQBhcq1tsh/YgbvNmDauQr5Q=", passwd)
28+
29+
module Passwords
30+
class KnownPasswords
31+
def include?(passwd)
32+
passwd == "foo"
33+
end
34+
end
35+
end
36+
37+
# Call to object method
38+
Passwords::KnownPasswords.new.include?("kdW/xVhiv6y1fQQNevDpUaq+2rfPKfh+teE/45zS7bc=")
39+
40+
# Call to unrelated method with same name (should not be flagged)
41+
"foobar".include?("foo")

0 commit comments

Comments
 (0)