ESLint Rule to discourage hashes being created with unsafe algorithms (#190973)

Closes https://github.com/elastic/kibana/issues/185601

## Summary

Using non-compliant algorithms with Node Cryptos createHash function
will cause failures when running Kibana in FIPS mode.

We want to discourage usages of such algorithms.

---------

Co-authored-by: Sid <siddharthmantri1@gmail.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Kurt 2024-09-30 12:34:04 -04:00 committed by GitHub
parent 7aa64b6ed5
commit f207c2c176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 320 additions and 10 deletions

View file

@ -19,5 +19,6 @@ module.exports = {
no_constructor_args_in_property_initializers: require('./rules/no_constructor_args_in_property_initializers'),
no_this_in_property_initializers: require('./rules/no_this_in_property_initializers'),
no_unsafe_console: require('./rules/no_unsafe_console'),
no_unsafe_hash: require('./rules/no_unsafe_hash'),
},
};

View file

@ -0,0 +1,166 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
const allowedAlgorithms = ['sha256', 'sha3-256', 'sha512'];
module.exports = {
allowedAlgorithms,
meta: {
type: 'problem',
docs: {
description: 'Allow usage of createHash only with allowed algorithms.',
category: 'FIPS',
recommended: false,
},
messages: {
noDisallowedHash:
'Usage of {{functionName}} with "{{algorithm}}" is not allowed. Only the following algorithms are allowed: [{{allowedAlgorithms}}]. If you need to use a different algorithm, please contact the Kibana security team.',
},
schema: [],
},
create(context) {
let isCreateHashImported = false;
let createHashName = 'createHash';
let cryptoLocalName = 'crypto';
let usedFunctionName = '';
const sourceCode = context.getSourceCode();
const disallowedAlgorithmNodes = new Set();
function isAllowedAlgorithm(algorithm) {
return allowedAlgorithms.includes(algorithm);
}
function isHashOrCreateHash(value) {
if (value === 'hash' || value === 'createHash') {
usedFunctionName = value;
return true;
}
return false;
}
function getIdentifierValue(node) {
const scope = sourceCode.getScope(node);
if (!scope) {
return;
}
const variable = scope.variables.find((variable) => variable.name === node.name);
if (variable && variable.defs.length > 0) {
const def = variable.defs[0];
if (
def.node.init &&
def.node.init.type === 'Literal' &&
!isAllowedAlgorithm(def.node.init.value)
) {
disallowedAlgorithmNodes.add(node.name);
return def.node.init.value;
}
}
}
return {
ImportDeclaration(node) {
if (node.source.value === 'crypto' || node.source.value === 'node:crypto') {
node.specifiers.forEach((specifier) => {
if (
specifier.type === 'ImportSpecifier' &&
isHashOrCreateHash(specifier.imported.name)
) {
isCreateHashImported = true;
createHashName = specifier.local.name; // Capture local name (renamed or not)
} else if (specifier.type === 'ImportDefaultSpecifier') {
cryptoLocalName = specifier.local.name;
}
});
}
},
VariableDeclarator(node) {
if (node.init && node.init.type === 'Literal' && !isAllowedAlgorithm(node.init.value)) {
disallowedAlgorithmNodes.add(node.id.name);
}
},
AssignmentExpression(node) {
if (
node.right.type === 'Literal' &&
node.right.value === 'md5' &&
node.left.type === 'Identifier'
) {
disallowedAlgorithmNodes.add(node.left.name);
}
},
CallExpression(node) {
const callee = node.callee;
if (
callee.type === 'MemberExpression' &&
callee.object.name === cryptoLocalName &&
isHashOrCreateHash(callee.property.name)
) {
const arg = node.arguments[0];
if (arg) {
if (arg.type === 'Literal' && !isAllowedAlgorithm(arg.value)) {
context.report({
node,
messageId: 'noDisallowedHash',
data: {
algorithm: arg.value,
allowedAlgorithms: allowedAlgorithms.join(', '),
functionName: usedFunctionName,
},
});
} else if (arg.type === 'Identifier') {
const identifierValue = getIdentifierValue(arg);
if (disallowedAlgorithmNodes.has(arg.name) && identifierValue) {
context.report({
node,
messageId: 'noDisallowedHash',
data: {
algorithm: identifierValue,
allowedAlgorithms: allowedAlgorithms.join(', '),
functionName: usedFunctionName,
},
});
}
}
}
}
if (isCreateHashImported && callee.name === createHashName) {
const arg = node.arguments[0];
if (arg) {
if (arg.type === 'Literal' && !isAllowedAlgorithm(arg.value)) {
context.report({
node,
messageId: 'noDisallowedHash',
data: {
algorithm: arg.value,
allowedAlgorithms: allowedAlgorithms.join(', '),
functionName: usedFunctionName,
},
});
} else if (arg.type === 'Identifier') {
const identifierValue = getIdentifierValue(arg);
if (disallowedAlgorithmNodes.has(arg.name) && identifierValue) {
context.report({
node,
messageId: 'noDisallowedHash',
data: {
algorithm: identifierValue,
allowedAlgorithms: allowedAlgorithms.join(', '),
functionName: usedFunctionName,
},
});
}
}
}
}
},
};
},
};

View file

@ -0,0 +1,142 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
const { RuleTester } = require('eslint');
const { allowedAlgorithms, ...rule } = require('./no_unsafe_hash');
const dedent = require('dedent');
const joinedAllowedAlgorithms = `[${allowedAlgorithms.join(', ')}]`;
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
ecmaFeatures: {
jsx: true,
},
},
});
ruleTester.run('@kbn/eslint/no_unsafe_hash', rule, {
valid: [
// valid import of crypto and call of createHash
{
code: dedent`
import crypto from 'crypto';
crypto.createHash('sha256');
`,
},
// valid import and call of createHash
{
code: dedent`
import { createHash } from 'crypto';
createHash('sha256');
`,
},
// valid import and call of createHash with a variable containing a compliant aglorithm
{
code: dedent`
import { createHash } from 'crypto';
const myHash = 'sha256';
createHash(myHash);
`,
},
// valid import and call of hash with a variable containing a compliant aglorithm
{
code: dedent`
import { hash } from 'crypto';
const myHash = 'sha256';
hash(myHash);
`,
},
],
invalid: [
// invalid call of createHash when calling from crypto
{
code: dedent`
import crypto from 'crypto';
crypto.createHash('md5');
`,
errors: [
{
line: 2,
message: `Usage of createHash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
},
],
},
// invalid call of createHash when importing directly
{
code: dedent`
import { createHash } from 'crypto';
createHash('md5');
`,
errors: [
{
line: 2,
message: `Usage of createHash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
},
],
},
// invalid call of createHash when calling with a variable containing md5
{
code: dedent`
import { createHash } from 'crypto';
const myHash = 'md5';
createHash(myHash);
`,
errors: [
{
line: 3,
message: `Usage of createHash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
},
],
},
// invalid import and call of hash when importing directly
{
code: dedent`
import { hash } from 'crypto';
hash('md5');
`,
errors: [
{
line: 2,
message: `Usage of hash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
},
],
},
{
code: dedent`
import _crypto from 'crypto';
_crypto.hash('md5');
`,
errors: [
{
line: 2,
message: `Usage of hash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
},
],
},
{
code: dedent`
import { hash as _hash } from 'crypto';
_hash('md5');
`,
errors: [
{
line: 2,
message: `Usage of hash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
},
],
},
],
});