Skip to content

Commit 4234cfe

Browse files
committed
Ruby: model CipherOperations for OpenSSL
1 parent 489391e commit 4234cfe

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed

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

Lines changed: 189 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,189 @@ 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+
// Convenience methods for getting constant value arguments in cipher instantiation
367+
private class CipherCallNode extends DataFlow::CallNode {
368+
string getStringArgument(int i) {
369+
result = super.getArgument(i).asExpr().getConstantValue().getStringOrSymbol()
370+
}
371+
372+
int getIntArgument(int i) { result = super.getArgument(i).asExpr().getConstantValue().getInt() }
373+
}
374+
375+
/** A call to `OpenSSL::Cipher.new` or similar. */
376+
private class CipherInstantiation extends CipherCallNode {
377+
private OpenSSLCipher cipher;
378+
private CipherMode cipherMode;
379+
380+
CipherInstantiation() {
381+
exists(string cipherName |
382+
// `OpenSSL::Cipher.new('<cipherName>')`
383+
this = cipherApi().getAnInstantiation() and
384+
cipherName = this.getStringArgument(0) and
385+
// CBC is used by default
386+
cipherMode.isBlockMode("CBC")
387+
or
388+
// `OpenSSL::Cipher::AES` instantiations
389+
this = cipherApi().getMember("AES").getAnInstantiation() and
390+
exists(string keyLength, string blockMode |
391+
// `OpenSSL::Cipher::AES.new('<keyLength-blockMode>')
392+
exists(string arg0 |
393+
arg0 = this.getStringArgument(0) and
394+
keyLength = arg0.splitAt("-", 0) and
395+
blockMode = arg0.splitAt("-", 1).toUpperCase()
396+
)
397+
or
398+
// `OpenSSL::Cipher::AES.new(<keyLength>, '<blockMode>')`
399+
keyLength = this.getIntArgument(0).toString() and
400+
blockMode = this.getStringArgument(1).toUpperCase()
401+
|
402+
cipherName = "AES-" + keyLength + "-" + blockMode and
403+
cipherMode.isBlockMode(blockMode)
404+
)
405+
or
406+
// RC4 stream cipher
407+
this = cipherApi().getMember("RC4").getAnInstantiation() and
408+
cipherMode = TStreamCipher() and
409+
(
410+
if exists(this.getStringArgument(0))
411+
then cipherName = "RC4-" + this.getStringArgument(0).toUpperCase()
412+
else cipherName = "RC4"
413+
)
414+
or
415+
// Block ciphers with dedicated modules
416+
exists(string mod, string blockAlgo |
417+
mod = ["AES128", "AES192", "AES256", "BF", "CAST5", "DES", "IDEA", "RC2"]
418+
|
419+
this = cipherApi().getMember(mod).getAnInstantiation() and
420+
(
421+
// The `AES<keyLength>` modules are a special case in terms of naming
422+
if mod = ["AES128", "AES192", "AES256"]
423+
then blockAlgo = "AES-" + mod.suffix(3)
424+
else blockAlgo = mod
425+
) and
426+
exists(string blockMode |
427+
if exists(this.getStringArgument(0))
428+
then
429+
// `OpenSSL::Cipher::<blockAlgo>.new('<blockMode>')`
430+
blockMode = this.getStringArgument(0).toUpperCase()
431+
else
432+
// `OpenSSL::Cipher::<blockAlgo>.new` uses CBC by default
433+
blockMode = "CBC"
434+
|
435+
cipherName = blockAlgo + "-" + blockMode and
436+
cipherMode.isBlockMode(blockMode)
437+
)
438+
)
439+
|
440+
cipher.matchesName(cipherName)
441+
)
442+
}
443+
444+
/** Gets the `OpenSSLCipher` associated with this instance. */
445+
OpenSSLCipher getCipher() { result = cipher }
446+
447+
/** Gets the mode used by this cipher, if applicable. */
448+
CipherMode getCipherMode() { result = cipherMode }
449+
}
450+
451+
private DataFlow::LocalSourceNode cipherInstance(
452+
TypeTracker t, OpenSSLCipher cipher, CipherMode cipherMode
453+
) {
454+
t.start() and
455+
result.(CipherInstantiation).getCipher() = cipher and
456+
result.(CipherInstantiation).getCipherMode() = cipherMode
457+
or
458+
exists(TypeTracker t2 | result = cipherInstance(t2, cipher, cipherMode).track(t2, t))
459+
}
460+
461+
/** A node with flow from `OpenSSL::Cipher.new`. */
462+
private class CipherNode extends DataFlow::Node {
463+
private OpenSSLCipher cipher;
464+
private CipherMode cipherMode;
465+
466+
CipherNode() { cipherInstance(TypeTracker::end(), cipher, cipherMode).flowsTo(this) }
467+
468+
/** Gets the cipher associated with this node. */
469+
OpenSSLCipher getCipher() { result = cipher }
470+
471+
/** Gets the cipher associated with this node. */
472+
CipherMode getCipherMode() { result = cipherMode }
473+
}
474+
475+
/** An operation using the OpenSSL library that uses a cipher. */
476+
private class CipherOperation extends Cryptography::CryptographicOperation::Range,
477+
DataFlow::CallNode {
478+
private CipherNode cipherNode;
479+
private DataFlow::Node input;
480+
481+
CipherOperation() {
482+
// cipher instantiation is counted as a cipher operation with no input
483+
cipherNode = this and cipherNode instanceof CipherInstantiation
484+
or
485+
this.getReceiver() = cipherNode and
486+
this.getMethodName() = "update" and
487+
input = this.getArgument(0)
488+
}
489+
490+
override Cryptography::EncryptionAlgorithm getAlgorithm() {
491+
result = cipherNode.getCipher().getAlgorithm()
492+
}
493+
494+
override DataFlow::Node getAnInput() { result = input }
495+
496+
override predicate isWeak() {
497+
cipherNode.getCipher().isWeak() or
498+
cipherNode.getCipherMode().isWeak()
499+
}
311500
}

0 commit comments

Comments
 (0)