Skip to content

Commit 4fdd072

Browse files
committed
WIP: HardcodedCredentials query
1 parent af6f050 commit 4fdd072

10 files changed

Lines changed: 432 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* @name Hard-coded credentials
3+
* @description Credentials are hard coded in the source code of the application.
4+
* @problem.severity error
5+
* @precision medium TODO
6+
* @id rb/hardcoded-credentials
7+
* @tags security
8+
* external/cwe/cwe-259
9+
* external/cwe/cwe-321
10+
* external/cwe/cwe-798
11+
*/
12+
13+
import ruby
14+
import codeql_ruby.DataFlow
15+
import DataFlow::PathGraph
16+
private import codeql_ruby.dataflow.SSA
17+
private import codeql_ruby.CFG
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 me
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+
private predicate isCredentialSink(Expr e) {
84+
exists(string name |
85+
name.regexpMatch(getACredentialRegex()) and
86+
not name.suffix(name.length() - 4) = "file"
87+
|
88+
// A method call with a parameter that may hold a credential
89+
exists(Method m, NamedParameter p, int idx, MethodCall mc |
90+
mc.getArgument(idx).getAChild*() = e and
91+
p = m.getParameter(idx) and
92+
p.getName() = name and
93+
// TODO: link call w/ method more precisely
94+
mc.getMethodName() = m.getName()
95+
)
96+
or
97+
// An equality check against a credential value
98+
exists(EqualityOperation op, VariableReadAccess vra | vra.getVariable().getName() = name |
99+
op.getLeftOperand() = e and op.getRightOperand() = vra
100+
or
101+
op.getLeftOperand() = vra and op.getRightOperand() = e
102+
)
103+
/*
104+
* or
105+
* exists(Keyword k | k.getArg() = name and k.getValue().getAFlowNode() = this)
106+
*/
107+
108+
)
109+
}
110+
111+
class CredentialSink extends DataFlow::Node {
112+
CredentialSink() { isCredentialSink(this.asExpr().getExpr()) }
113+
}
114+
115+
class HardcodedCredentialsConfiguration extends DataFlow::Configuration {
116+
HardcodedCredentialsConfiguration() { this = "HardcodedCredentialsConfiguration" }
117+
118+
override predicate isSource(DataFlow::Node source) { source instanceof HardcodedValueSource }
119+
120+
override predicate isSink(DataFlow::Node sink) { sink instanceof CredentialSink }
121+
}
122+
123+
from Method m, NamedParameter p, int idx, MethodCall mc, Expr e
124+
where
125+
mc.getArgument(idx) = e and
126+
p = m.getParameter(idx) and
127+
// TODO: link call w/ method more precisely
128+
mc.getMethodName() = m.getName()
129+
select m, p, idx, mc, e
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: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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 medium
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+
// TODO: adjust precision
15+
16+
import ruby
17+
import codeql_ruby.DataFlow
18+
import DataFlow::PathGraph
19+
private import codeql_ruby.dataflow.SSA
20+
private import codeql_ruby.CFG
21+
22+
bindingset[char, fraction]
23+
predicate fewer_characters_than(StringLiteral str, string char, float fraction) {
24+
exists(string text, int chars |
25+
text = str.getValueText() and
26+
chars = count(int i | text.charAt(i) = char)
27+
|
28+
/* Allow one character */
29+
chars = 1 or
30+
chars < text.length() * fraction
31+
)
32+
}
33+
34+
predicate possible_reflective_name(string name) {
35+
// TODO: implement this?
36+
none()
37+
}
38+
39+
int char_count(StringLiteral str) { result = count(string c | c = str.getValueText().charAt(_)) }
40+
41+
predicate capitalized_word(StringLiteral str) { str.getValueText().regexpMatch("[A-Z][a-z]+") }
42+
43+
predicate format_string(StringLiteral str) { str.getValueText().matches("%{%}%") }
44+
45+
predicate maybeCredential(Expr e) {
46+
/* A string that is not too short and unlikely to be text or an identifier. */
47+
exists(StringLiteral str | str = e |
48+
/* At least 10 characters */
49+
str.getValueText().length() > 9 and
50+
/* Not too much whitespace */
51+
fewer_characters_than(str, " ", 0.05) and
52+
/* or underscores */
53+
fewer_characters_than(str, "_", 0.2) and
54+
/* Not too repetitive */
55+
exists(int chars | chars = char_count(str) |
56+
chars > 15 or
57+
chars * 3 > str.getValueText().length() * 2
58+
) and
59+
not possible_reflective_name(str.getValueText()) and
60+
not capitalized_word(str) and
61+
not format_string(str)
62+
)
63+
or
64+
/* Or, an integer with over 32 bits */
65+
exists(IntegerLiteral lit | lit = e |
66+
not exists(lit.getValue()) and
67+
/* Not a set of flags or round number */
68+
not lit.getValueText().matches("%00%")
69+
)
70+
}
71+
72+
class HardcodedValueSource extends DataFlow::Node {
73+
HardcodedValueSource() { maybeCredential(this.asExpr().getExpr()) }
74+
}
75+
76+
/**
77+
* Gets a regular expression for matching names of locations (variables, parameters, keys) that
78+
* indicate the value being held is a credential.
79+
*/
80+
private string getACredentialRegex() {
81+
result = "(?i).*pass(wd|word|code|phrase)(?!.*question).*" or
82+
result = "(?i).*(puid|username|userid).*" or
83+
result = "(?i).*(cert)(?!.*(format|name)).*"
84+
}
85+
86+
private predicate isCredentialSink(Expr e) {
87+
exists(string name |
88+
name.regexpMatch(getACredentialRegex()) and
89+
not name.suffix(name.length() - 4) = "file"
90+
|
91+
// A method call with a parameter that may hold a credential
92+
exists(Method m, NamedParameter p, int idx, MethodCall mc |
93+
// Include keyword argument values etc.
94+
mc.getArgument(idx).getAChild*() = e and
95+
p = m.getParameter(idx) and
96+
p.getName() = name and
97+
// TODO: link call w/ method more precisely
98+
mc.getMethodName() = m.getName()
99+
)
100+
or
101+
// An equality check against a credential value
102+
exists(EqualityOperation op, VariableReadAccess vra | vra.getVariable().getName() = name |
103+
op.getLeftOperand() = e and op.getRightOperand() = vra
104+
or
105+
op.getLeftOperand() = vra and op.getRightOperand() = e
106+
)
107+
)
108+
}
109+
110+
class CredentialSink extends DataFlow::Node {
111+
CredentialSink() { isCredentialSink(this.asExpr().getExpr()) }
112+
}
113+
114+
class HardcodedCredentialsConfiguration extends DataFlow::Configuration {
115+
HardcodedCredentialsConfiguration() { this = "HardcodedCredentialsConfiguration" }
116+
117+
override predicate isSource(DataFlow::Node source) { source instanceof HardcodedValueSource }
118+
119+
override predicate isSink(DataFlow::Node sink) { sink instanceof CredentialSink }
120+
}
121+
122+
from DataFlow::PathNode source, DataFlow::PathNode sink, HardcodedCredentialsConfiguration conf
123+
where conf.hasFlowPath(source, sink)
124+
select sink.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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import ruby
2+
3+
/*
4+
* predicate possible_reflective_name(string name) {
5+
* exists(any(ModuleValue m).attr(name))
6+
* or
7+
* exists(any(ClassValue c).lookup(name))
8+
* or
9+
* any(ClassValue c).getName() = name
10+
* or
11+
* exists(Module::named(name))
12+
* or
13+
* exists(Value::named(name))
14+
* }
15+
*/
16+
17+
string module_name() { result = any(Namespace m | | m.getName()) }
18+
19+
from string s
20+
where s = module_name()
21+
select s

0 commit comments

Comments
 (0)