Skip to content

Commit ccd7bb5

Browse files
authored
Merge pull request #8421 from alexrford/ruby/weak-cryptographic-algorithm
Ruby: Add `rb/weak-cryptographic-algorithm` query
2 parents 1f72f6c + 8b0ebbf commit ccd7bb5

File tree

9 files changed

+476
-0
lines changed

9 files changed

+476
-0
lines changed

ruby/ql/lib/codeql/ruby/Concepts.qll

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,3 +816,54 @@ module Logging {
816816
abstract DataFlow::Node getAnInput();
817817
}
818818
}
819+
820+
/**
821+
* Provides models for cryptographic concepts.
822+
*
823+
* Note: The `CryptographicAlgorithm` class currently doesn't take weak keys into
824+
* consideration for the `isWeak` member predicate. So RSA is always considered
825+
* secure, although using a low number of bits will actually make it insecure. We plan
826+
* to improve our libraries in the future to more precisely capture this aspect.
827+
*/
828+
module Cryptography {
829+
import security.CryptoAlgorithms
830+
831+
/**
832+
* A data-flow node that is an application of a cryptographic algorithm. For example,
833+
* encryption, decryption, signature-validation.
834+
*
835+
* Extend this class to refine existing API models. If you want to model new APIs,
836+
* extend `CryptographicOperation::Range` instead.
837+
*/
838+
class CryptographicOperation extends DataFlow::Node instanceof CryptographicOperation::Range {
839+
/** Gets the algorithm used, if it matches a known `CryptographicAlgorithm`. */
840+
CryptographicAlgorithm getAlgorithm() { result = super.getAlgorithm() }
841+
842+
/** Gets an input the algorithm is used on, for example the plain text input to be encrypted. */
843+
DataFlow::Node getAnInput() { result = super.getAnInput() }
844+
845+
/** Holds if this encryption operation is known to be weak. */
846+
predicate isWeak() { super.isWeak() }
847+
}
848+
849+
/** Provides classes for modeling new applications of a cryptographic algorithms. */
850+
module CryptographicOperation {
851+
/**
852+
* A data-flow node that is an application of a cryptographic algorithm. For example,
853+
* encryption, decryption, signature-validation.
854+
*
855+
* Extend this class to model new APIs. If you want to refine existing API models,
856+
* extend `CryptographicOperation` instead.
857+
*/
858+
abstract class Range extends DataFlow::Node {
859+
/** Gets the algorithm used, if it matches a known `CryptographicAlgorithm`. */
860+
abstract CryptographicAlgorithm getAlgorithm();
861+
862+
/** Gets an input the algorithm is used on, for example the plain text input to be encrypted. */
863+
abstract DataFlow::Node getAnInput();
864+
865+
/** Holds if this encryption operation is known to be weak. */
866+
abstract predicate isWeak();
867+
}
868+
}
869+
}

ruby/ql/lib/codeql/ruby/security/OpenSSL.qll

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
*/
55

66
private import internal.CryptoAlgorithmNames
7+
private import codeql.ruby.Concepts
8+
private import codeql.ruby.DataFlow
9+
private import codeql.ruby.ApiGraphs
10+
private import codeql.ruby.typetracking.TypeTracker
711

812
bindingset[algorithmString]
913
private string algorithmRegex(string algorithmString) {
@@ -308,4 +312,241 @@ class OpenSSLCipher extends MkOpenSSLCipher {
308312

309313
/** Gets a textual representation of this element. */
310314
string toString() { result = this.getCanonicalName() }
315+
316+
/** Holds if the specified name represents this cipher. */
317+
bindingset[candidateName]
318+
predicate matchesName(string candidateName) {
319+
this.getCanonicalName() = getCanonicalCipherName(candidateName)
320+
}
321+
322+
/** Gets the encryption algorithm used by this cipher. */
323+
Cryptography::EncryptionAlgorithm getAlgorithm() { result.matchesName(this.getCanonicalName()) }
324+
}
325+
326+
/** `OpenSSL::Cipher` or `OpenSSL::Cipher::Cipher` */
327+
private API::Node cipherApi() {
328+
result = API::getTopLevelMember("OpenSSL").getMember("Cipher") or
329+
result = API::getTopLevelMember("OpenSSL").getMember("Cipher").getMember("Cipher")
330+
}
331+
332+
private class BlockMode extends string {
333+
BlockMode() { this = ["ECB", "CBC", "GCM", "CCM", "CFB", "OFB", "CTR"] }
334+
}
335+
336+
private newtype TCipherMode =
337+
TStreamCipher() or
338+
TBlockMode(BlockMode blockMode)
339+
340+
/**
341+
* Represents the mode used by this stream cipher.
342+
* If this cipher uses a block encryption algorithm, then this is a specific
343+
* block mode.
344+
*/
345+
private class CipherMode extends TCipherMode {
346+
private BlockMode getBlockMode() { this = TBlockMode(result) }
347+
348+
/** Gets a textual representation of this node. */
349+
string toString() {
350+
result = this.getBlockMode()
351+
or
352+
this = TStreamCipher() and result = "<stream cipher>"
353+
}
354+
355+
/**
356+
* Holds if the string `s`, after normalization, represents the block mode
357+
* used by this cipher.
358+
*/
359+
bindingset[s]
360+
predicate isBlockMode(string s) { this.getBlockMode() = s.toUpperCase() }
361+
362+
/** Holds if this cipher mode is a weak block mode. */
363+
predicate isWeak() { isWeakBlockMode(this.getBlockMode()) }
364+
}
365+
366+
private string getStringArgument(DataFlow::CallNode call, int i) {
367+
result = call.getArgument(i).asExpr().getConstantValue().getStringlikeValue()
368+
}
369+
370+
private int getIntArgument(DataFlow::CallNode call, int i) {
371+
result = call.getArgument(i).asExpr().getConstantValue().getInt()
372+
}
373+
374+
/**
375+
* Holds if `call` is a call to `OpenSSL::Cipher.new` that instantiates a
376+
* `cipher` instance with mode `cipherMode`.
377+
*/
378+
private predicate cipherInstantiationGeneric(
379+
DataFlow::CallNode call, OpenSSLCipher cipher, CipherMode cipherMode
380+
) {
381+
exists(string cipherName | cipher.matchesName(cipherName) |
382+
// `OpenSSL::Cipher.new('<cipherName>')`
383+
call = cipherApi().getAnInstantiation() and
384+
cipherName = getStringArgument(call, 0) and
385+
// CBC is used by default
386+
cipherMode.isBlockMode("CBC")
387+
)
388+
}
389+
390+
/**
391+
* Holds if `call` is a call to `OpenSSL::Cipher::AES.new` or
392+
* `OpenSSL::Cipher::AES{128,192,256}.new` that instantiates an AES `cipher` instance
393+
* with mode `cipherMode`.
394+
*/
395+
private predicate cipherInstantiationAES(
396+
DataFlow::CallNode call, OpenSSLCipher cipher, CipherMode cipherMode
397+
) {
398+
exists(string cipherName | cipher.matchesName(cipherName) |
399+
// `OpenSSL::Cipher::AES` instantiations
400+
call = cipherApi().getMember("AES").getAnInstantiation() and
401+
exists(string keyLength, string blockMode |
402+
// `OpenSSL::Cipher::AES.new('<keyLength-blockMode>')
403+
exists(string arg0 |
404+
arg0 = getStringArgument(call, 0) and
405+
keyLength = arg0.splitAt("-", 0) and
406+
blockMode = arg0.splitAt("-", 1).toUpperCase()
407+
)
408+
or
409+
// `OpenSSL::Cipher::AES.new(<keyLength>, '<blockMode>')`
410+
keyLength = getIntArgument(call, 0).toString() and
411+
blockMode = getStringArgument(call, 1).toUpperCase()
412+
|
413+
cipherName = "AES-" + keyLength + "-" + blockMode and
414+
cipherMode.isBlockMode(blockMode)
415+
)
416+
or
417+
// Modules for AES with specific key lengths
418+
exists(string mod, string blockAlgo | mod = ["AES128", "AES192", "AES256"] |
419+
call = cipherApi().getMember(mod).getAnInstantiation() and
420+
// Canonical representation is `AES-<keyLength>`
421+
blockAlgo = "AES-" + mod.suffix(3) and
422+
exists(string blockMode |
423+
if exists(getStringArgument(call, 0))
424+
then
425+
// `OpenSSL::Cipher::<blockAlgo>.new('<blockMode>')`
426+
blockMode = getStringArgument(call, 0).toUpperCase()
427+
else
428+
// `OpenSSL::Cipher::<blockAlgo>.new` uses CBC by default
429+
blockMode = "CBC"
430+
|
431+
cipherName = blockAlgo + "-" + blockMode and
432+
cipherMode.isBlockMode(blockMode)
433+
)
434+
)
435+
)
436+
}
437+
438+
/**
439+
* Holds if `call` is a call that instantiates an OpenSSL cipher using a module
440+
* specific to a block encryption algorithm, e.g. Blowfish, DES, etc.
441+
*/
442+
private predicate cipherInstantiationSpecific(
443+
DataFlow::CallNode call, OpenSSLCipher cipher, CipherMode cipherMode
444+
) {
445+
exists(string cipherName | cipher.matchesName(cipherName) |
446+
// Block ciphers with dedicated modules
447+
exists(string blockAlgo | blockAlgo = ["BF", "CAST5", "DES", "IDEA", "RC2"] |
448+
call = cipherApi().getMember(blockAlgo).getAnInstantiation() and
449+
exists(string blockMode |
450+
if exists(getStringArgument(call, 0))
451+
then
452+
// `OpenSSL::Cipher::<blockAlgo>.new('<blockMode>')`
453+
blockMode = getStringArgument(call, 0).toUpperCase()
454+
else
455+
// `OpenSSL::Cipher::<blockAlgo>.new` uses CBC by default
456+
blockMode = "CBC"
457+
|
458+
cipherName = blockAlgo + "-" + blockMode and
459+
cipherMode.isBlockMode(blockMode)
460+
)
461+
)
462+
)
463+
}
464+
465+
/**
466+
* Holds if `call` is a call to `OpenSSL::Cipher::RC4.new` or an RC4 `cipher`
467+
* instance with mode `cipherMode`.
468+
*/
469+
private predicate cipherInstantiationRC4(
470+
DataFlow::CallNode call, OpenSSLCipher cipher, CipherMode cipherMode
471+
) {
472+
exists(string cipherName | cipher.matchesName(cipherName) |
473+
// RC4 stream cipher
474+
call = cipherApi().getMember("RC4").getAnInstantiation() and
475+
cipherMode = TStreamCipher() and
476+
(
477+
if exists(getStringArgument(call, 0))
478+
then cipherName = "RC4-" + getStringArgument(call, 0).toUpperCase()
479+
else cipherName = "RC4"
480+
)
481+
)
482+
}
483+
484+
/** A call to `OpenSSL::Cipher.new` or similar. */
485+
private class CipherInstantiation extends DataFlow::CallNode {
486+
private OpenSSLCipher cipher;
487+
private CipherMode cipherMode;
488+
489+
CipherInstantiation() {
490+
cipherInstantiationGeneric(this, cipher, cipherMode) or
491+
cipherInstantiationAES(this, cipher, cipherMode) or
492+
cipherInstantiationSpecific(this, cipher, cipherMode) or
493+
cipherInstantiationRC4(this, cipher, cipherMode)
494+
}
495+
496+
/** Gets the `OpenSSLCipher` associated with this instance. */
497+
OpenSSLCipher getCipher() { result = cipher }
498+
499+
/** Gets the mode used by this cipher, if applicable. */
500+
CipherMode getCipherMode() { result = cipherMode }
501+
}
502+
503+
private DataFlow::LocalSourceNode cipherInstance(
504+
TypeTracker t, OpenSSLCipher cipher, CipherMode cipherMode
505+
) {
506+
t.start() and
507+
result.(CipherInstantiation).getCipher() = cipher and
508+
result.(CipherInstantiation).getCipherMode() = cipherMode
509+
or
510+
exists(TypeTracker t2 | result = cipherInstance(t2, cipher, cipherMode).track(t2, t))
511+
}
512+
513+
/** A node with flow from `OpenSSL::Cipher.new`. */
514+
private class CipherNode extends DataFlow::Node {
515+
private OpenSSLCipher cipher;
516+
private CipherMode cipherMode;
517+
518+
CipherNode() { cipherInstance(TypeTracker::end(), cipher, cipherMode).flowsTo(this) }
519+
520+
/** Gets the cipher associated with this node. */
521+
OpenSSLCipher getCipher() { result = cipher }
522+
523+
/** Gets the cipher associated with this node. */
524+
CipherMode getCipherMode() { result = cipherMode }
525+
}
526+
527+
/** An operation using the OpenSSL library that uses a cipher. */
528+
private class CipherOperation extends Cryptography::CryptographicOperation::Range,
529+
DataFlow::CallNode {
530+
private CipherNode cipherNode;
531+
private DataFlow::Node input;
532+
533+
CipherOperation() {
534+
// cipher instantiation is counted as a cipher operation with no input
535+
cipherNode = this and cipherNode instanceof CipherInstantiation
536+
or
537+
this.getReceiver() = cipherNode and
538+
this.getMethodName() = "update" and
539+
input = this.getArgument(0)
540+
}
541+
542+
override Cryptography::EncryptionAlgorithm getAlgorithm() {
543+
result = cipherNode.getCipher().getAlgorithm()
544+
}
545+
546+
override DataFlow::Node getAnInput() { result = input }
547+
548+
override predicate isWeak() {
549+
cipherNode.getCipher().isWeak() or
550+
cipherNode.getCipherMode().isWeak()
551+
}
311552
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new query, `rb/weak-cryptographic-algorithm`. The query finds uses of cryptographic algorithms that are known to be weak, such as DES.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
Using broken or weak cryptographic algorithms can leave data
8+
vulnerable to being decrypted or forged by an attacker.
9+
</p>
10+
<p>
11+
Many cryptographic algorithms provided by cryptography
12+
libraries are known to be weak, or flawed. Using such an
13+
algorithm means that encrypted or hashed data is less
14+
secure than it appears to be.
15+
</p>
16+
</overview>
17+
<recommendation>
18+
<p>
19+
Ensure that you use a strong, modern cryptographic
20+
algorithm, such as AES-128 or RSA-2048.
21+
</p>
22+
</recommendation>
23+
<example>
24+
25+
<p>
26+
The following code uses the <code>OpenSSL</code> library to encrypt some
27+
secret data. When you create a cipher using <code>OpenSSL</code> you must
28+
specify the encryption algorithm to use. The first example uses DES, which
29+
is an older algorithm that is now considered weak. The second example uses
30+
AES, which is a stronger modern algorithm.
31+
</p>
32+
33+
<sample src="examples/broken_crypto.rb" />
34+
</example>
35+
<references>
36+
<li>NIST, FIPS 140 Annex a: <a href="http://csrc.nist.gov/publications/fips/fips140-2/fips1402annexa.pdf"> Approved Security Functions</a>.</li>
37+
<li>NIST, SP 800-131A: <a href="http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar1.pdf"> Transitions: Recommendation for Transitioning the Use of Cryptographic Algorithms and Key Lengths</a>.</li>
38+
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#rule---use-strong-approved-authenticated-encryption">Rule - Use strong approved cryptographic algorithms</a>.
39+
</li>
40+
</references>
41+
</qhelp>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @name Use of a broken or weak cryptographic algorithm
3+
* @description Using broken or weak cryptographic algorithms can compromise security.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @security-severity 7.5
7+
* @precision high
8+
* @id rb/weak-cryptographic-algorithm
9+
* @tags security
10+
* external/cwe/cwe-327
11+
*/
12+
13+
import ruby
14+
import codeql.ruby.Concepts
15+
16+
from Cryptography::CryptographicOperation operation
17+
where operation.isWeak()
18+
select operation,
19+
"The cryptographic algorithm " + operation.getAlgorithm().getName() +
20+
" is broken or weak, and should not be used."
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'openssl'
2+
3+
class Encryptor
4+
attr_accessor :secret_key
5+
6+
def encrypt_message_weak(message)
7+
cipher = OpenSSL::Cipher.new('des') # BAD: weak encryption
8+
cipher.encrypt
9+
cipher.key = secret_key
10+
cipher.update(message)
11+
cipher.final
12+
end
13+
14+
def encrypt_message_strong(message)
15+
cipher = OpenSSL::Cipher::AES128.new # GOOD: strong encryption
16+
cipher.encrypt
17+
cipher.key = secret_key
18+
cipher.update(message)
19+
cipher.final
20+
end
21+
end

0 commit comments

Comments
 (0)