|
4 | 4 | */
|
5 | 5 |
|
6 | 6 | 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 |
7 | 11 |
|
8 | 12 | bindingset[algorithmString]
|
9 | 13 | private string algorithmRegex(string algorithmString) {
|
@@ -308,4 +312,241 @@ class OpenSSLCipher extends MkOpenSSLCipher {
|
308 | 312 |
|
309 | 313 | /** Gets a textual representation of this element. */
|
310 | 314 | 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 | + } |
311 | 552 | }
|
0 commit comments