Skip to content

Commit

Permalink
Add rules around Node.js crypto module (#3357)
Browse files Browse the repository at this point in the history
Co-authored-by: Pieter De Cremer (Semgrep) <[email protected]>
  • Loading branch information
Starkteetje and 0xDC0DE authored Oct 15, 2024
1 parent b4eb008 commit c865e0c
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 0 deletions.
114 changes: 114 additions & 0 deletions javascript/node-crypto/security/aead-no-final.js
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");
}
37 changes: 37 additions & 0 deletions javascript/node-crypto/security/aead-no-final.yaml
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)$"
53 changes: 53 additions & 0 deletions javascript/node-crypto/security/create-de-cipher-no-iv.js
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 javascript/node-crypto/security/create-de-cipher-no-iv.yaml
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(...)
84 changes: 84 additions & 0 deletions javascript/node-crypto/security/gcm-no-tag-length.js
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;
}
33 changes: 33 additions & 0 deletions javascript/node-crypto/security/gcm-no-tag-length.yaml
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)$"

0 comments on commit c865e0c

Please sign in to comment.