diff --git a/.gitignore b/.gitignore index 2f31239..3c29b15 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules yarn.lock .nyc_output/ .vscode/ +.idea/ diff --git a/index.js b/index.js index adf7d16..9bc289b 100644 --- a/index.js +++ b/index.js @@ -3,22 +3,53 @@ const path = require('path'); const importModules = require('import-modules'); -module.exports = { +const plugin = { + meta: { + name: 'eslint-plugin-risxss', + version: '1.0.0' + }, rules: importModules(path.resolve(__dirname, 'rules'), { camelize: false }), - configs: { - recommended: { - env: { - es6: true - }, - parserOptions: { - ecmaVersion: 2019, - sourceType: 'module' - }, - plugins: ['risxss'], - rules: { - 'risxss/catch-potential-xss-react': 'error', - 'risxss/catch-potential-xss-vue': 'error' + configs: {} +}; + +// Создаем конфигурацию для ESLint 9 (flat config) +plugin.configs.recommended = { + name: 'risxss/recommended', + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parserOptions: { + ecmaFeatures: { + jsx: true } } + }, + plugins: { + risxss: plugin + }, + rules: { + 'risxss/catch-potential-xss-react': 'error', + 'risxss/catch-potential-xss-vue': 'error' } }; + +// Для совместимости с ESLint 8 (legacy config) +plugin.configs.legacy = { + env: { + es6: true + }, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + }, + plugins: ['risxss'], + rules: { + 'risxss/catch-potential-xss-react': 'error', + 'risxss/catch-potential-xss-vue': 'error' + } +}; + +module.exports = plugin; diff --git a/package.json b/package.json index 9987a03..e60b7a7 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,24 @@ { - "name": "eslint-plugin-risxss", - "version": "2.1.0", + "name": "eslint-plugin-risxss-flat", + "version": "2.2.0", "description": "Various XSS-hunter ESLint rules", - "author": { - "name": "Guillaume Klaus", - "email": "guillaumek@theodo.fr" - }, + "author": "SergoDrovski", "scripts": { "test": "nyc ava", "lint": "xo", "integration": "./test/integration/test.js" }, + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin", + "xss", + "security", + "react", + "vue" + ], "engines": { - "node": ">=6" + "node": ">=16.0.0" }, "files": [ "index.js", @@ -20,10 +26,10 @@ ], "repository": { "type": "git", - "url": "https://github.com/theodo/RisXSS" + "url": "https://github.com/SergoDrovski/RisXSS" }, "dependencies": { - "import-modules": "^1.1.0", + "import-modules": "^2.1.0", "lodash.clonedeep": "^4.5.0", "lodash.get": "^4.4.2", "lodash.union": "^4.6.0" @@ -42,6 +48,9 @@ "vue-eslint-parser": "^7.6.0", "xo": "^0.36.1" }, + "peerDependencies": { + "eslint": ">=9.0.0" + }, "ava": { "files": [ "test/*.js" diff --git a/readme.md b/readme.md index 0a4a5cf..48dd7dd 100644 --- a/readme.md +++ b/readme.md @@ -125,6 +125,102 @@ export const DesktopPostCard = ({ post }) => ( ); ``` +### ESLint 9 (Flat Config) + +### `risxss/catch-potential-xss-react` + +```javascript +// eslint.config.js +import risxss from 'eslint-plugin-risxss-eslint9'; + +export default [ + { + files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], + plugins: { + risxss + }, + rules: { + 'risxss/catch-potential-xss-react': [ + 'error', + { + trustedLibraries: ['@frontend/core/shared/lib/cleanHTML/safeHTML'] + } + ], + 'risxss/catch-potential-xss-vue': 'error' + }, + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + } +]; +``` + +### Using a ready-made configuration + +```javascript +// eslint.config.js +import risxss from 'eslint-plugin-risxss-eslint9'; + +export default [ + risxss.configs.recommended, + { + rules: { + 'risxss/catch-potential-xss-react': [ + 'error', + { + trustedLibraries: ['@frontend/core/shared/lib/cleanHTML/safeHTML'] + } + ] + } + } +]; +``` + +### `risxss/catch-potential-xss-vue` + +#### Examples of unsafe code + +```vue + +``` + +#### Examples of secure code + +```vue + + + +``` ## License MIT diff --git a/rules/catch-potential-xss-react.js b/rules/catch-potential-xss-react.js index f5eb062..a1a4912 100644 --- a/rules/catch-potential-xss-react.js +++ b/rules/catch-potential-xss-react.js @@ -28,45 +28,67 @@ const create = context => { options = context.options[0]; } let isVariableTrusted = utils.defaultTrustedCall; + return { Program(node) { try { isVariableTrusted = utils.checkProgramNode(node, isVariableTrusted, options); } catch (error) { - context.report(node, `${utils.ERROR_MESSAGE} \n ${error.stack}`); + context.report({ + node, + message: `${utils.ERROR_MESSAGE} \n ${error.stack}` + }); } }, JSXAttribute(node) { try { if (isDangerouslySetInnerHTMLAttribute(node)) { if (get(node, 'value.type', '') !== 'JSXExpressionContainer') { - context.report(node, DANGEROUS_MESSAGE); + context.report({ + node, + message: DANGEROUS_MESSAGE + }); return; } const expression = get(node, 'value.expression', ''); switch (expression.type) { case 'Literal': - context.report(node, DANGEROUS_MESSAGE); + context.report({ + node, + message: DANGEROUS_MESSAGE + }); break; case 'ObjectExpression': if (!isInnerHTMLObjectExpressionSafe(expression, isVariableTrusted)) { - context.report(node, DANGEROUS_MESSAGE); + context.report({ + node, + message: DANGEROUS_MESSAGE + }); } break; case 'CallExpression': if (!utils.isVariableSafe(utils.getNameFromExpression(expression), isVariableTrusted, [])) { - context.report(node, DANGEROUS_MESSAGE); + context.report({ + node, + message: DANGEROUS_MESSAGE + }); } break; default: const variableName = `${utils.getNameFromExpression(expression)}.__html`; if (!utils.isVariableSafe(variableName, isVariableTrusted, [])) { - context.report(node, DANGEROUS_MESSAGE); + context.report({ + node, + message: DANGEROUS_MESSAGE + }); } } } } catch (error) { - context.report(node, `${utils.ERROR_MESSAGE} \n ${error.stack}`); + context.report({ + node, + message: `${utils.ERROR_MESSAGE} \n ${error.stack}` + }); } }, }; @@ -75,7 +97,26 @@ const create = context => { module.exports = { create, meta: { - type: 'suggestion', - fixable: 'code', + type: 'problem', + docs: { + description: 'Detect potential XSS vulnerabilities in React dangerouslySetInnerHTML', + category: 'Security', + recommended: true + }, + fixable: null, + schema: [ + { + type: 'object', + properties: { + trustedLibraries: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + } + ] }, }; diff --git a/rules/catch-potential-xss-vue.js b/rules/catch-potential-xss-vue.js index 7abf30c..612a34f 100644 --- a/rules/catch-potential-xss-vue.js +++ b/rules/catch-potential-xss-vue.js @@ -55,6 +55,7 @@ const create = context => { options = context.options[0]; } let isVariableTrusted = utils.defaultTrustedCall; + // The script visitor is called first. Then the template visitor return utils.defineTemplateBodyVisitor( context, @@ -69,17 +70,29 @@ const create = context => { if (expression && expression !== null) { const variableName = utils.getNameFromExpression(expression); if (!utils.isVariableSafe(variableName, isVariableTrusted, [])) { - context.report(node, DANGEROUS_MESSAGE); + context.report({ + node, + message: DANGEROUS_MESSAGE + }); } } else { - context.report(node, DANGEROUS_MESSAGE); + context.report({ + node, + message: DANGEROUS_MESSAGE + }); } } else { - context.report(node, DANGEROUS_MESSAGE); + context.report({ + node, + message: DANGEROUS_MESSAGE + }); } } } catch (error) { - context.report(node, `${utils.ERROR_MESSAGE} \n ${error.stack}`); + context.report({ + node, + message: `${utils.ERROR_MESSAGE} \n ${error.stack}` + }); } }, }, @@ -90,7 +103,10 @@ const create = context => { isVariableTrusted = utils.checkProgramNode(node, isVariableTrusted, options); isVariableTrusted = postProcessVariablesForVue(isVariableTrusted); } catch (error) { - context.report(node, `${utils.ERROR_MESSAGE} \n ${error.stack}`); + context.report({ + node, + message: `${utils.ERROR_MESSAGE} \n ${error.stack}` + }); } }, // Check export default with Vue.extend() @@ -98,7 +114,10 @@ const create = context => { try { isVariableTrusted = checkVueExportDefaultDeclaration(node, isVariableTrusted); } catch (error) { - context.report(node, `${utils.ERROR_MESSAGE} \n ${error.stack}`); + context.report({ + node, + message: `${utils.ERROR_MESSAGE} \n ${error.stack}` + }); } }, }, @@ -108,7 +127,26 @@ const create = context => { module.exports = { create, meta: { - type: 'suggestion', - fixable: 'code', + type: 'problem', + docs: { + description: 'Detect potential XSS vulnerabilities in Vue v-html directive', + category: 'Security', + recommended: true + }, + fixable: null, + schema: [ + { + type: 'object', + properties: { + trustedLibraries: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + } + ] }, }; diff --git a/rules/utils/index.js b/rules/utils/index.js index 003d431..4a27d68 100644 --- a/rules/utils/index.js +++ b/rules/utils/index.js @@ -1,4 +1,4 @@ -'use-strict'; +'use strict'; const ERROR_MESSAGE = 'The linter couldn\'t lint the file properly, please open an issue on the RisXSS repo. \n The error is : ';