|
| 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" |
0 commit comments