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 : ';