-
Notifications
You must be signed in to change notification settings - Fork 414
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add rules around Node.js crypto module (#3357)
Co-authored-by: Pieter De Cremer (Semgrep) <[email protected]>
- Loading branch information
1 parent
b4eb008
commit c865e0c
Showing
6 changed files
with
352 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
const crypto = require('node:crypto') | ||
function decrypt1(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
|
||
// ruleid: aead-no-final | ||
const decipher = crypto.createDecipheriv("aes-128-gcm", key, iv); | ||
let result = decipher.update(encryptedData); | ||
|
||
return result.toString("utf8"); | ||
} | ||
|
||
algo = "aes-192-gcm" | ||
function decrypt2(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// ruleid: aead-no-final | ||
var decipher = crypto.createDecipheriv(algo, key, iv); | ||
decipher.setAuthTag(auth); | ||
let result = decipher.update(encryptedData); | ||
|
||
return result.toString("utf8"); | ||
} | ||
|
||
function decrypt3(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// ruleid: aead-no-final | ||
const decipher = crypto.createDecipheriv("aes-256-ccm", key, iv, {authTagLength: 16}) | ||
decipher.setAuthTag(auth) | ||
decipher.setAAD(Buffer.alloc(0), {plaintextLength: encryptedData.length}) | ||
let result = decipher.update(encryptedData) | ||
|
||
return result; | ||
} | ||
|
||
function decrypt4(key, ciphertext) { | ||
let encrypted = Buffer.from(ciphertext, 'base64'); | ||
let iv = encrypted.slice(encrypted.byteLength - 12, encrypted.byteLength); | ||
|
||
// ruleid: aead-no-final | ||
let decipher = crypto.createDecipheriv("aes-256-ocb", key, iv, {authTagLength: 16}); | ||
let decrypted = decipher.update(encrypted.slice(0, encrypted.byteLength - 16 - 12)); | ||
|
||
decrypted = Buffer.concat([decrypted]); | ||
return decrypted; | ||
} | ||
|
||
const { createDecipheriv } = require('node:crypto') | ||
function decrypt5(key, ciphertext) { | ||
let iv = ciphertext.slice(ciphertext.byteLength - 12, ciphertext.byteLength); | ||
|
||
// ruleid: aead-no-final | ||
let decipher = createDecipheriv("chacha20-poly1305", key, iv); | ||
let decrypted = decipher.update(ciphertext.slice(0, ciphertext.byteLength - 16 - 12)); | ||
|
||
decrypted = Buffer.concat([decrypted]); | ||
return decrypted; | ||
} | ||
|
||
function decrypt6(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// ok: aead-no-final | ||
var decipher = crypto.createDecipheriv("aes-128-gcm", key, iv); | ||
decipher.setAuthTag(auth); | ||
let result = Buffer.concat([decipher.update(encryptedData), decipher.final()]); | ||
|
||
return result; | ||
} | ||
|
||
function decrypt7(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// ok: aead-no-final | ||
var decipher = crypto.createDecipheriv("aes-192-ccm", key, iv, {authTagLength: 16}) | ||
decipher.setAuthTag(auth) | ||
let result = decipher.update(encryptedData) | ||
result += decipher.final() | ||
|
||
return result | ||
} | ||
|
||
function decrypt8(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// ok: aead-no-final | ||
var decipher = crypto.createDecipheriv("aes-256-ocb", key, iv, {authTagLength: 16}); | ||
decipher.setAuthTag(auth); | ||
return decipher.update(encryptedData) + decipher.final(); | ||
} | ||
|
||
function decrypt9(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// missing final, but not AEAD mode | ||
// ok: aead-no-final | ||
var decipher = crypto.createDecipheriv("aes-192-cbc", key, iv); | ||
let result = decipher.update(encryptedData); | ||
|
||
return result.toString("utf8"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
rules: | ||
- id: aead-no-final | ||
message: >- | ||
The 'final' call of a Decipher object checks the authentication tag in a mode for authenticated encryption. | ||
Failing to call 'final' will invalidate all integrity guarantees of the released ciphertext. | ||
metadata: | ||
cwe: | ||
- 'CWE-310: CWE CATEGORY: Cryptographic Issues' | ||
owasp: | ||
- A02:2021 - Cryptographic Failures | ||
category: security | ||
subcategory: | ||
- vuln | ||
technology: | ||
- node-crypto | ||
likelihood: HIGH | ||
impact: MEDIUM | ||
confidence: HIGH | ||
references: | ||
- https://nodejs.org/api/crypto.html#deciphersetauthtagbuffer-encoding | ||
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures/ | ||
languages: | ||
- javascript | ||
- typescript | ||
severity: ERROR | ||
patterns: | ||
- pattern: | | ||
$DECIPHER = $CRYPTO.createDecipheriv('$ALGO', ...) | ||
... | ||
$DECIPHER.update(...) | ||
- pattern-not-inside: | | ||
$DECIPHER = $CRYPTO.createDecipheriv('$ALGO', ...) | ||
... | ||
$DECIPHER.final(...) | ||
- metavariable-regex: | ||
metavariable: $ALGO | ||
regex: ".*(-gcm|-ccm|-ocb|chacha20-poly1305)$" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
const crypto = require('node:crypto') | ||
function decrypt1(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
|
||
// ruleid: create-de-cipher-no-iv | ||
const decipher = crypto.createDecipher("aes-128-gcm", key, iv); | ||
let result = decipher.update(encryptedData); | ||
|
||
return result.toString("utf8"); | ||
} | ||
|
||
function decrypt2(ciphertext, key) { | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// ruleid: create-de-cipher-no-iv | ||
var decipher = crypto.createDecipher("aes-192-cbc", key); | ||
let result = decipher.update(encryptedData); | ||
|
||
return result.toString("utf8"); | ||
} | ||
|
||
const { createCipher } = require('node:crypto') | ||
function encrypt1(plaintext, key) { | ||
// ruleid: create-de-cipher-no-iv | ||
const cipher = createCipher("aes-256-ccm", key, {authTagLength: 16}) | ||
cipher.setAAD(Buffer.alloc(0), {plaintextLength: plaintext.length}) | ||
let result = cipher.update(plaintext) + cipher.final() | ||
|
||
return result + cipher.getAuthTag() | ||
} | ||
|
||
function decrypt3(key, ciphertext) { | ||
let encrypted = Buffer.from(ciphertext, 'base64'); | ||
let iv = encrypted.slice(encrypted.byteLength - 12, encrypted.byteLength); | ||
|
||
// ok: create-de-cipher-no-iv | ||
let decipher = crypto.createDecipheriv("aes-256-ocb", key, iv, {authTagLength: 16}); | ||
let decrypted = decipher.update(encrypted.slice(0, encrypted.byteLength - 16 - 12)); | ||
|
||
decrypted = Buffer.concat([decrypted]); | ||
return decrypted; | ||
} | ||
|
||
function encrypt2(key, plaintext) { | ||
let iv = crypto.randomBytes(12); | ||
|
||
// ok: create-de-cipher-no-iv | ||
let cipher = crypto.createCipheriv("chacha20-poly1305", key, iv); | ||
let encrypted = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]); | ||
return encrypted; | ||
} |
31 changes: 31 additions & 0 deletions
31
javascript/node-crypto/security/create-de-cipher-no-iv.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
rules: | ||
- id: create-de-cipher-no-iv | ||
message: >- | ||
The deprecated functions 'createCipher' and 'createDecipher' generate the same initialization vector every time. | ||
For counter modes such as CTR, GCM, or CCM this leads to break of both confidentiality and integrity, if the key is used more than once. | ||
Other modes are still affected in their strength, though they're not completely broken. | ||
Use 'createCipheriv' or 'createDecipheriv' instead. | ||
metadata: | ||
cwe: | ||
- 'CWE-1204: Generation of Weak Initialization Vector (IV)' | ||
category: security | ||
subcategory: | ||
- vuln | ||
technology: | ||
- node-crypto | ||
likelihood: HIGH | ||
impact: MEDIUM | ||
confidence: HIGH | ||
references: | ||
- https://nodejs.org/api/crypto.html#cryptocreatecipheralgorithm-password-options | ||
- https://nodejs.org/api/crypto.html#cryptocreatedecipheralgorithm-password-options | ||
languages: | ||
- javascript | ||
- typescript | ||
severity: ERROR | ||
patterns: | ||
- pattern-either: | ||
- pattern: | | ||
$CRYPTO.createCipher(...) | ||
- pattern: | | ||
$CRYPTO.createDecipher(...) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
const crypto = require('node:crypto') | ||
function decrypt1(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
algo = "aes-128-gcm" | ||
|
||
// ruleid: gcm-no-tag-length | ||
const decipher = crypto.createDecipheriv(algo, key, iv); | ||
decipher.setAuthTag(auth); | ||
let result = decipher.update(encryptedData) + decipher.final(); | ||
|
||
return result.toString("utf8"); | ||
} | ||
|
||
function decrypt2(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
// While this prevents the attack, I'm not quite sure how to capture such length checks and exclude them from the findings | ||
assert(auth.length === 16) | ||
|
||
// ruleid: gcm-no-tag-length | ||
const decipher = crypto.createDecipheriv("aes-192-gcm", key, iv); | ||
decipher.setAuthTag(auth); | ||
let result = decipher.update(encryptedData) + decipher.final(); | ||
|
||
return result.toString("utf8"); | ||
} | ||
|
||
function decrypt3(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// ok: gcm-no-tag-length | ||
var decipher = crypto.createDecipheriv("aes-128-gcm", key, iv, {authTagLength: 16}); | ||
decipher.setAuthTag(auth); | ||
let result = Buffer.concat([decipher.update(encryptedData), decipher.final()]); | ||
|
||
return result; | ||
} | ||
|
||
function decrypt4(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// even though auth tag is shorter than it should be, this looks like a conscious choice | ||
// ok: gcm-no-tag-length | ||
var decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {authTagLength: 4}); | ||
decipher.setAuthTag(auth); | ||
let result = Buffer.concat([decipher.update(encryptedData), decipher.final()]); | ||
|
||
return result; | ||
} | ||
|
||
function decrypt5(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// hard to capture whether options contain 'authTagLength', so to reduce false positives, just ignore any call with options | ||
// ok: gcm-no-tag-length | ||
var decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {someOption: someValue}) | ||
decipher.setAuthTag(auth) | ||
let result = decipher.update(encryptedData)+ decipher.final() | ||
|
||
return result; | ||
} | ||
|
||
function decrypt6(ciphertext, key) { | ||
iv = ciphertext.iv | ||
encryptedData = ciphertext.data | ||
auth = ciphertext.auth | ||
|
||
// ok: gcm-no-tag-length | ||
var decipher = crypto.createDecipheriv("chacha20-poly1305", key, iv) | ||
decipher.setAuthTag(auth) | ||
let result = decipher.update(encryptedData)+ decipher.final() | ||
|
||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
rules: | ||
- id: gcm-no-tag-length | ||
message: >- | ||
The call to 'createDecipheriv' with the Galois Counter Mode (GCM) mode of operation is missing an expected authentication tag length. | ||
If the expected authentication tag length is not specified or otherwise checked, the application might be tricked into verifying a shorter-than-expected authentication tag. | ||
This can be abused by an attacker to spoof ciphertexts or recover the implicit authentication key of GCM, allowing arbitrary forgeries. | ||
metadata: | ||
cwe: | ||
- 'CWE-310: CWE CATEGORY: Cryptographic Issues' | ||
owasp: | ||
- A02:2021 - Cryptographic Failures | ||
category: security | ||
subcategory: | ||
- vuln | ||
technology: | ||
- node-crypto | ||
likelihood: MEDIUM | ||
impact: MEDIUM | ||
confidence: MEDIUM | ||
references: | ||
- https://www.securesystems.de/blog/forging_ciphertexts_under_Galois_Counter_Mode_for_the_Node_js_crypto_module/ | ||
- https://nodejs.org/api/crypto.html#cryptocreatedecipherivalgorithm-key-iv-options | ||
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures/ | ||
languages: | ||
- javascript | ||
- typescript | ||
severity: ERROR | ||
patterns: | ||
- pattern: | | ||
$CRYPTO.createDecipheriv('$ALGO', $KEY, $IV) | ||
- metavariable-regex: | ||
metavariable: $ALGO | ||
regex: ".*(-gcm)$" |