diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dab80590..a4360889 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,5 +11,5 @@ jobs: - uses: yandex-cloud/ui-release-action@main with: github-token: ${{ secrets.YC_UI_BOT_GITHUB_TOKEN }} - npm-token: ${{ secrets.YC_UI_BOT_NPM_TOKEN }} + npm-token: ${{ secrets.ROBOT_DATAUI_NPM_TOKEN }} node-version: 14 diff --git a/CHANGELOG.diplodoc.md b/CHANGELOG.diplodoc.md new file mode 100644 index 00000000..23c0d805 --- /dev/null +++ b/CHANGELOG.diplodoc.md @@ -0,0 +1,38 @@ +# Changelog @diplodoc/transform@4.0.0 + +## It's major update of @doc-tools/transform@3.11.0 with security changes. + +### New term's linter + +The main feature of term is generating a hidden content, that will be show on click. Terms plugins creates MarkadownIt tokens at place, where term was defined and it can brake our `@doc-tools/docs` navigation. Now `@diplodoc/transform` has new yfmlint rule: `no-term-definition-in-content`. There are several restrictions: - You can't define content between term-def - All term-def should be placed at the end of file. + +### Enabling `needToSanitizeHtml` by default + +The sanitizer includes default options with safe, allowed tags, and attributes. However, by default, both the `style` tag and the `style` attribute are also allowed. The values will be processed by the [cssfilter](https://github.com/leizongmin/js-css-filter) module to prevent XSS attacks. The cssfilter module includes a default CSS whitelist. + +You can override the options for sanitizer like this: + +```javascript +const transform = require('@doc-tools/transform'); +const {defaultOptions} = require('@doc-tools/transform/lib/sanitize'); + +const sanitizeOptions = Object.assign({}, defaultOptions); + +// Allow css property +sanitizeOptions.cssWhiteList['position'] = true; + +// Disallow css property +delete sanitizeOptions.cssWhiteList['color']; + +// Disable `style` tag +sanitizeOptions.allowedTags = sanitizeOptions.allowedTags.filter((tag) => tag !== 'style'); + +// Disable `style` attribute +sanitizeOptions.allowedAttributes['*'] = sanitizeOptions.allowedAttributes['*'].filter( + (attr) => attr !== 'style', +); + +const { + result: {html}, +} = transform(content, {sanitizeOptions}); +``` diff --git a/package-lock.json b/package-lock.json index 39c00cbc..d4e2fde1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "@doc-tools/transform", - "version": "3.11.0", + "name": "@diplodoc/transform", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4248,6 +4248,12 @@ "@babel/types": "^7.3.0" } }, + "@types/css": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/css/-/css-0.0.34.tgz", + "integrity": "sha512-nyZiWXt6gBG7Jj7BYB4zdHXyrhcAjJH8qv+BUsO8FMJR5be7H5ETOaibB3uvXeX5lc57LWkecNJv03q0+JpbXA==", + "dev": true + }, "@types/github-slugger": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@types/github-slugger/-/github-slugger-1.3.0.tgz", @@ -4687,6 +4693,11 @@ "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", "dev": true }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, "autoprefixer": { "version": "10.4.15", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", @@ -4894,6 +4905,11 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5002,6 +5018,114 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + } + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + } + } + }, "chokidar": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", @@ -5044,7 +5168,7 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true }, "collect-v8-coverage": { @@ -5110,6 +5234,16 @@ "which": "^2.0.1" } }, + "css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "requires": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, "css-blank-pseudo": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-6.0.0.tgz", @@ -5136,6 +5270,58 @@ "integrity": "sha512-03QGAk/FXIRseDdLb7XAiu6gidQ0Nd8945xuM7VFVPpc6goJsG9uIO8xQjTxwbPdPIIV4o4AJoOJyt8gwDl67g==", "dev": true }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + } + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "cssdb": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.7.0.tgz", @@ -5148,6 +5334,11 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" + }, "debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -5157,10 +5348,15 @@ "ms": "2.1.2" } }, + "decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, "deep-eql": { @@ -5765,7 +5961,7 @@ "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true }, "expect": { @@ -5815,7 +6011,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "fastq": { @@ -6148,7 +6344,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, "inflight": { @@ -6164,8 +6360,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "internal-slot": { "version": "1.0.3", @@ -6930,7 +7125,7 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "json5": { @@ -7002,13 +7197,13 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, "lodash.merge": { @@ -7188,7 +7383,7 @@ "mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "merge-stream": { "version": "2.0.0", @@ -7241,13 +7436,13 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, "node-releases": { @@ -7277,6 +7472,14 @@ "path-key": "^3.0.0" } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7432,6 +7635,40 @@ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + }, + "dependencies": { + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + } + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "dependencies": { + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + } + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8174,7 +8411,7 @@ "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", "dev": true, "requires": { "ret": "~0.1.10" @@ -8269,14 +8506,22 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, + "source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, "source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -8443,7 +8688,7 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, "tmpl": { @@ -8603,7 +8848,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, "v8-compile-cache": { diff --git a/package.json b/package.json index a5c59049..6774a32b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@doc-tools/transform", - "version": "3.11.0", + "name": "@diplodoc/transform", + "version": "4.0.0", "description": "A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML", "author": "YFM Team ", "license": "MIT", @@ -27,6 +27,9 @@ "dependencies": { "@diplodoc/tabs-extension": "^2.0.11", "chalk": "4.1.2", + "cheerio": "^1.0.0-rc.12", + "css": "^3.0.0", + "cssfilter": "0.0.10", "get-root-node-polyfill": "1.0.0", "github-slugger": "1.4.0", "js-yaml": "^4.1.0", @@ -45,6 +48,7 @@ "@babel/core": "7.18.10", "@babel/plugin-transform-modules-commonjs": "7.18.6", "@babel/preset-env": "7.18.10", + "@types/css": "0.0.34", "@types/github-slugger": "1.3.0", "@types/jest": "28.1.7", "@types/js-yaml": "^4.0.5", diff --git a/src/transform/log.ts b/src/transform/log.ts index 27b5e482..22f06682 100644 --- a/src/transform/log.ts +++ b/src/transform/log.ts @@ -18,7 +18,7 @@ function createLogger(type: LogLevels) { const formatter: Record string> = { [LogLevels.INFO]: (msg) => `${green('INFO')} ${msg}`, [LogLevels.WARN]: (msg) => `${yellow('WARN')} ${msg}`, - [LogLevels.ERROR]: (msg) => `${red('ERR ')} ${msg}`, + [LogLevels.ERROR]: (msg) => `${red('ERR')} ${msg}`, }; return function log(msg: string) { diff --git a/src/transform/md.ts b/src/transform/md.ts index 245d17c6..c46568be 100644 --- a/src/transform/md.ts +++ b/src/transform/md.ts @@ -111,7 +111,7 @@ function initParser(md: MarkdownIt, options: OptionsType, env: EnvType) { } function initCompiler(md: MarkdownIt, options: OptionsType, env: EnvType) { - const {needToSanitizeHtml = false, sanitizeOptions} = options; + const {needToSanitizeHtml = true, sanitizeOptions} = options; return (tokens: Token[]) => { const html = md.renderer.render(tokens, md.options, env); diff --git a/src/transform/plugins/code.ts b/src/transform/plugins/code.ts index 4f62ba38..49405f0e 100644 --- a/src/transform/plugins/code.ts +++ b/src/transform/plugins/code.ts @@ -15,7 +15,7 @@ const wrapInClipboard = (element: string | undefined, id: number) => { diff --git a/src/transform/sanitize.ts b/src/transform/sanitize.ts index 292eac92..36f58340 100644 --- a/src/transform/sanitize.ts +++ b/src/transform/sanitize.ts @@ -1,4 +1,8 @@ import sanitizeHtml from 'sanitize-html'; +// @ts-ignore +import cssfilter from 'cssfilter'; +import * as cheerio from 'cheerio'; +import css from 'css'; const htmlTags = [ 'a', @@ -45,7 +49,6 @@ const htmlTags = [ 'figure', 'font', 'footer', - 'form', 'h1', 'h2', 'h3', @@ -118,6 +121,7 @@ const htmlTags = [ 'video', 'wbr', 'iframe', + 'style', ]; const svgTags = [ @@ -279,10 +283,15 @@ const htmlAttrs = [ 'scrolling', 'allow', 'target', + 'attributeName', + 'aria-hidden', + 'referrerpolicy', + 'aria-describedby', 'data-*', ]; const svgAttrs = [ + 'viewBox', 'accent-height', 'accumulate', 'additive', @@ -468,23 +477,120 @@ const svgAttrs = [ 'to', ]; +const yfmHtmlAttrs = ['note-type', 'yfm2xliff-explicit', 'term-key']; + const allowedTags = Array.from( new Set([...htmlTags, ...svgTags, ...sanitizeHtml.defaults.allowedTags]), ); -const allowedAttributes = Array.from(new Set([...htmlAttrs, ...svgAttrs])); +const allowedAttributes = Array.from(new Set([...htmlAttrs, ...svgAttrs, ...yfmHtmlAttrs])); -type DefaultOptions = sanitizeHtml.IDefaults; -export type SanitizeOptions = sanitizeHtml.IOptions; +export type CssWhiteList = {[property: string]: boolean}; -export const defaultOptions: DefaultOptions = { +export interface SanitizeOptions extends sanitizeHtml.IOptions { + cssWhiteList?: CssWhiteList; +} + +export const defaultParseOptions = { + lowerCaseAttributeNames: false, +}; + +export const defaultOptions: SanitizeOptions = { ...sanitizeHtml.defaults, allowedTags, allowedAttributes: { ...sanitizeHtml.defaults.allowedAttributes, '*': allowedAttributes, }, + allowVulnerableTags: true, + parser: defaultParseOptions, + cssWhiteList: cssfilter.whiteList, }; +function sanitizeStyleTags(dom: cheerio.CheerioAPI, cssWhiteList: CssWhiteList) { + const styleTags = dom('style'); + + styleTags.each((_index: number, element: cheerio.Element) => { + const styleText = dom(element).text(); + + try { + const parsedCSS = css.parse(styleText); + + if (!parsedCSS.stylesheet) { + return; + } + + parsedCSS.stylesheet.rules = parsedCSS.stylesheet.rules.filter( + (rule: css.Rule) => rule.type === 'rule', + ); + + parsedCSS.stylesheet.rules.forEach((rule: css.Rule) => { + if (!rule.declarations) { + return; + } + + rule.declarations = rule.declarations.filter((declaration: css.Declaration) => { + if (!declaration.property || !declaration.value) { + return false; + } + + const isWhiteListed = cssWhiteList[declaration.property]; + + if (isWhiteListed) { + declaration.value = cssfilter.safeAttrValue( + declaration.property, + declaration.value, + ); + } + + if (!declaration.value) { + return false; + } + + return isWhiteListed; + }); + }); + + dom(element).text(css.stringify(parsedCSS)); + } catch {} + }); +} + +function sanitizeStyleAttrs(dom: cheerio.CheerioAPI, cssWhiteList: CssWhiteList) { + const options = { + whiteList: cssWhiteList, + }; + const cssSanitizer = new cssfilter.FilterCSS(options); + + dom('*').each((_index, element) => { + const styleAttrValue = dom(element).attr('style'); + + if (!styleAttrValue) { + return; + } + + dom(element).attr('style', cssSanitizer.process(styleAttrValue)); + }); +} + +function sanitizeStyles(html: string, options: SanitizeOptions) { + const cssWhiteList = options.cssWhiteList || {}; + + const $ = cheerio.load(html); + + sanitizeStyleTags($, cssWhiteList); + + sanitizeStyleAttrs($, cssWhiteList); + + const styles = $('head').html() || ''; + const content = $('body').html() || ''; + + return styles + content; +} + export default function sanitize(html: string, options?: SanitizeOptions) { - return sanitizeHtml(html, options || defaultOptions); + const sanitizeOptions = options || defaultOptions; + + const modifiedHtml = sanitizeStyles(html, sanitizeOptions); + + return sanitizeHtml(modifiedHtml, sanitizeOptions); } diff --git a/src/transform/yfmlint/index.ts b/src/transform/yfmlint/index.ts index e75df24b..ea26096a 100644 --- a/src/transform/yfmlint/index.ts +++ b/src/transform/yfmlint/index.ts @@ -19,8 +19,9 @@ import {errorToString, getLogLevel} from './utils'; import {Options} from './typings'; import {Dictionary} from 'lodash'; import {Logger, LogLevels} from '../log'; +import {yfm009} from './markdownlint-custom-rule/yfm009'; -const defaultLintRules = [yfm001, yfm002, yfm003, yfm004, yfm005, yfm006, yfm007, yfm008]; +const defaultLintRules = [yfm001, yfm002, yfm003, yfm004, yfm005, yfm006, yfm007, yfm008, yfm009]; const lintCache = new Set(); diff --git a/src/transform/yfmlint/markdownlint-custom-rule/index.ts b/src/transform/yfmlint/markdownlint-custom-rule/index.ts index 52194af1..1f5f5b92 100644 --- a/src/transform/yfmlint/markdownlint-custom-rule/index.ts +++ b/src/transform/yfmlint/markdownlint-custom-rule/index.ts @@ -6,3 +6,4 @@ export {yfm005} from './yfm005'; export {yfm006} from './yfm006'; export {yfm007} from './yfm007'; export {yfm008} from './yfm008'; +export {yfm009} from './yfm009'; diff --git a/src/transform/yfmlint/markdownlint-custom-rule/yfm009.ts b/src/transform/yfmlint/markdownlint-custom-rule/yfm009.ts new file mode 100644 index 00000000..0a01d9ea --- /dev/null +++ b/src/transform/yfmlint/markdownlint-custom-rule/yfm009.ts @@ -0,0 +1,54 @@ +import {Rule} from 'markdownlint'; + +export const yfm009: Rule = { + names: ['YFM009', 'no-term-definition-in-content'], + description: 'Term definition should be placed at the end of file', + tags: ['term'], + function: function YFM009(params, onError) { + const {config} = params; + if (!config) { + return; + } + + let lastCloseIndex = -1; + const size = params.tokens.length; + + for (let i = 0; i < size; i++) { + if (params.tokens[i].type === 'template_close') { + lastCloseIndex = i; + } + + if (params.tokens[i].type !== 'template_close') { + continue; + } + + if (i === size - 1) { + continue; + } + + if (params.tokens[i + 1].type === 'template_open') { + continue; + } + + if (i < size - 2 && params.tokens[i + 2].type === 'template_open') { + continue; + } + + onError({ + lineNumber: params.tokens[i + 1].lineNumber, + detail: 'There is a content between term definition. All term defitions should be placed at the end of file.', + }); + } + + if (lastCloseIndex === -1) { + return; + } + + if (lastCloseIndex !== size - 1) { + onError({ + lineNumber: params.tokens[lastCloseIndex + 1].lineNumber, + detail: 'The file must end with term only.', + }); + } + }, +}; diff --git a/src/transform/yfmlint/yfmlint.ts b/src/transform/yfmlint/yfmlint.ts index 94fb29c4..ec54669d 100644 --- a/src/transform/yfmlint/yfmlint.ts +++ b/src/transform/yfmlint/yfmlint.ts @@ -61,6 +61,7 @@ const index: LintConfig = { YFM006: LogLevels.WARN, // Term definition duplicated YFM007: LogLevels.WARN, // Term used without definition YFM008: LogLevels.WARN, // Term inside definition not allowed + YFM009: LogLevels.ERROR, // Term definition used not at the end of file }, // Inline code length diff --git a/test/__snapshots__/highlight-code.test.ts.snap b/test/__snapshots__/highlight-code.test.ts.snap index 7839e2bc..2df71d72 100644 --- a/test/__snapshots__/highlight-code.test.ts.snap +++ b/test/__snapshots__/highlight-code.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should highlight code 1`] = ` -"
const x: string = 'y';
+"
const x: string = 'y';
 
" `; diff --git a/test/checkbox.test.ts b/test/checkbox.test.ts index 618efff5..fc4c352a 100644 --- a/test/checkbox.test.ts +++ b/test/checkbox.test.ts @@ -82,7 +82,7 @@ describe('markdown-it-checkbox', function () { it('should parse inline markup in label', () => { expect(transformYfm('[X] text *italic* **bold** label')).toBe( '
\n' + - '\n' + + '\n' + '\n' + '
\n', ); diff --git a/test/file.test.ts b/test/file.test.ts index 4403925d..1c960b4f 100644 --- a/test/file.test.ts +++ b/test/file.test.ts @@ -28,12 +28,8 @@ describe('File plugin', () => { }); it('should not render file without all required attrs', () => { - expect(transform('{% file src="../file" %}')).toBe( - '

{% file src="../file" %}

\n', - ); - expect(transform('{% file name="file.txt" %}')).toBe( - '

{% file name="file.txt" %}

\n', - ); + expect(transform('{% file src="../file" %}')).toBe('

{% file src="../file" %}

\n'); + expect(transform('{% file name="file.txt" %}')).toBe('

{% file name="file.txt" %}

\n'); }); it('should render file with text before', () => { @@ -118,15 +114,9 @@ describe('File plugin', () => { ); }); - it('should escape attrs', () => { - expect(transform('{% file src="ind${iconHtml}ind"ex.ht&ml

\n`, - ); - }); - it('should allow quoutes in attribute value', () => { expect(transform('{% file src="ind\'ex.txt" name=\'ind"ex.html\' %}')).toBe( - `

${iconHtml}ind"ex.html

\n`, + `

${iconHtml}ind"ex.html

\n`, ); }); @@ -140,7 +130,7 @@ describe('File plugin', () => { it('should not render file without spaces around attrs', () => { expect(transform('{% file src="index.txt"name="index.html"type="text/html" %}')).toBe( - `

{% file src="index.txt"name="index.html"type="text/html" %}

\n`, + `

{% file src="index.txt"name="index.html"type="text/html" %}

\n`, ); }); diff --git a/test/markdownlint-custom-rules/yfm004.test.ts b/test/markdownlint-custom-rules/yfm004.test.ts index 0ccf6dc4..9484b660 100644 --- a/test/markdownlint-custom-rules/yfm004.test.ts +++ b/test/markdownlint-custom-rules/yfm004.test.ts @@ -22,10 +22,12 @@ const tableWithCloseToken = ` |# `.trim(); +const random = () => Math.floor(Math.random() * 1e8); + const lint = (input: string) => { yfmlint({ input, - pluginOptions: {log, path: 'test.md'}, + pluginOptions: {log, path: `${random()}.md`}, lintConfig: { 'log-levels': { MD047: LogLevels.DISABLED, diff --git a/test/markdownlint-custom-rules/yfm009.test.ts b/test/markdownlint-custom-rules/yfm009.test.ts new file mode 100644 index 00000000..79cbcf0c --- /dev/null +++ b/test/markdownlint-custom-rules/yfm009.test.ts @@ -0,0 +1,68 @@ +import {log, LogLevels} from '../../src/transform/log'; +import term from '../../src/transform/plugins/term'; +import yfmlint from '../../src/transform/yfmlint'; +import {yfm009} from '../../src/transform/yfmlint/markdownlint-custom-rule'; + +const withIncludes = `\ +[*widget-popup1]: {% include [ ](./_includes/widget.md) %} + +[*button-popup2]: {% include [ ](./_includes/button) %} + +[*button-popup3]: {% include [ ](./_includes/button) %}`.trim(); + +const plainText = `\ +[*widget-popup1]: hello world + +hi i am text + +[*widget-popup2]: it's will fail`.trim(); + +const appendText = (text: string) => { + return `${text}\n\nhi i am text after`; +}; + +const prependText = (text: string) => { + return `hi i am text before\n\n${text}`; +}; + +const random = () => Math.floor(Math.random() * 1e8); + +const lint = (input: string) => { + yfmlint({ + input, + pluginOptions: {log, path: `${random()}.md`}, + lintConfig: { + 'log-levels': { + YFM009: LogLevels.ERROR, + }, + }, + customLintRules: [yfm009], + plugins: [term], + }); +}; + +describe('YFM009', () => { + beforeEach(() => { + log.clear(); + }); + + it('not accepts text between terms', () => { + lint(plainText); + expect(log.get().error[0]).toMatch('YFM009'); + }); + + it('not accepts includes with text after', () => { + lint(appendText(withIncludes)); + expect(log.get().error[0]).toMatch('YFM009'); + }); + + it('accepts includes', () => { + lint(withIncludes); + expect(log.isEmpty()).toBeTruthy(); + }); + + it('accepts includes with text before', () => { + lint(prependText(withIncludes)); + expect(log.isEmpty()).toBeTruthy(); + }); +}); diff --git a/test/sanitize-html.test.ts b/test/sanitize-html.test.ts index 997e392c..69ff1095 100644 --- a/test/sanitize-html.test.ts +++ b/test/sanitize-html.test.ts @@ -1,5 +1,5 @@ import transform from '../src/transform'; -import sanitizeHtml from '../src/transform/sanitize'; +import sanitizeHtml, {defaultOptions} from '../src/transform/sanitize'; const transformYfm = (text: string, options?: Parameters[1]) => { const { @@ -16,13 +16,117 @@ describe('Sanitize HTML utility', () => { expect(sanitizeHtml('')).toBe(''); }); - it('transform should sanitize html', () => { - expect(transformYfm('', {needToSanitizeHtml: true})).toBe( - '', - ); + describe('transform should sanitize html by default l', () => { + describe('html in markdown', () => { + it('should sanitize danger attributes', () => { + expect(transformYfm('')).toBe(''); + }); + + it('should sanitize danger style attributes', () => { + expect(transformYfm('
')).toBe( + '
', + ); + }); + + it('should sanitize danger properties in style tag', () => { + expect(transformYfm('')).toBe( + '', + ); + }); + + it('should sanitize form tag', () => { + expect( + transformYfm('
'), + ).toBe(''); + }); + }); + + describe('plugin markdown-it-attrs', () => { + it('should sanitize danger attributes', () => { + expect(transformYfm('Click {onfocus="alert(1)" onclick="alert(1)"}')).toBe( + '

Click

\n', + ); + }); + + it('should not sanitize safe attributes', () => { + expect(transformYfm('Click {.style-me data-toggle=modal}')).toBe( + '

Click

\n', + ); + }); + + it('should sanitize danger style attributes', () => { + expect( + transformYfm( + '[example.com](https://example.com){style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: red; opacity: 0.5"}', + ), + ).toBe( + '

example.com

\n', + ); + }); + }); }); - it('by default transform should not sanitize html', () => { - expect(transformYfm('')).toBe(''); + describe('rewrite default sanitize options', () => { + it('should not sanitize form tag if form is allowed', () => { + const sanitizeOptions = Object.assign({}, defaultOptions); + + // @ts-ignore + sanitizeOptions.allowedTags = sanitizeOptions.allowedTags.concat(['form']); + + expect( + transformYfm('
', { + sanitizeOptions, + }), + ).toBe('
'); + }); + + it('should filter style tag', () => { + const sanitizeOptions = Object.assign({}, defaultOptions); + + // @ts-ignore + sanitizeOptions.allowedTags = sanitizeOptions.allowedTags.filter( + (tag: string) => tag !== 'style', + ); + + expect( + transformYfm('', { + sanitizeOptions, + }), + ).toBe(''); + }); + + it('should filter style attribute', () => { + const sanitizeOptions = Object.assign({}, defaultOptions); + + // @ts-ignore + sanitizeOptions.allowedAttributes['*'] = sanitizeOptions.allowedAttributes['*'].filter( + (attr: string) => attr !== 'style', + ); + + expect( + transformYfm('
', { + sanitizeOptions, + }), + ).toBe('
'); + }); + + it('should not sanitize property if it is passed in cssWhiteList', () => { + const sanitizeOptions = Object.assign({}, defaultOptions); + + // @ts-ignore + sanitizeOptions.cssWhiteList['position'] = true; + + expect( + transformYfm('', { + sanitizeOptions, + }), + ).toBe(''); + }); + }); + + it('transform should not sanitize html if needToSanitizeHtml is false', () => { + expect(transformYfm('', {needToSanitizeHtml: false})).toBe( + '', + ); }); }); diff --git a/test/table.test.ts b/test/table.test.ts index 91ddf78f..75bd74b9 100644 --- a/test/table.test.ts +++ b/test/table.test.ts @@ -150,7 +150,7 @@ describe('Table plugin', () => { '\n' + '\n' + '\n' + - '

alt text

\n' + + '

alt text

\n' + '\n' + '\n' + '

link to README.md

\n' + @@ -187,7 +187,7 @@ describe('Table plugin', () => { '\n' + '\n' + '\n' + - '

Text with
\n' + + '

Text with
\n' + 'new line

\n' + '\n' + '\n' + @@ -208,7 +208,7 @@ describe('Table plugin', () => { '
  • Element B
  • \n' + '\n' + '\n' + - '
  • Element 2
    \n' + + '
  • Element 2
    \n' + 'test
  • \n' + '\n' + '\n' + @@ -349,13 +349,12 @@ describe('Table plugin', () => { '|Cell in column 2, row 3||', ), ).toBe( - '\n' + - '

    #|
    \n' + - '||Cell in column 1, row 1
    \n' + - '|Cell in column 2, row 1||
    \n' + - '||Cell in column 1, row 2
    \n' + - '|Cell in column 2, row 2||
    \n' + - '||Cell in column 1, row 3
    \n' + + '

    #|
    \n' + + '||Cell in column 1, row 1
    \n' + + '|Cell in column 2, row 1||
    \n' + + '||Cell in column 1, row 2
    \n' + + '|Cell in column 2, row 2||
    \n' + + '||Cell in column 1, row 3
    \n' + '|Cell in column 2, row 3||

    \n', ); }); @@ -371,10 +370,10 @@ describe('Table plugin', () => { 'Test', ), ).toBe( - '

    ||Cell in column 1, row 1
    \n' + - '|Cell in column 2, row 1
    \n' + - '||Cell in column 1, row 2
    \n' + - '|Cell in column 2, row 2
    \n' + + '

    ||Cell in column 1, row 1
    \n' + + '|Cell in column 2, row 1
    \n' + + '||Cell in column 1, row 2
    \n' + + '|Cell in column 2, row 2
    \n' + '|#

    \n' + '

    Test

    \n', ); @@ -392,12 +391,12 @@ describe('Table plugin', () => { '|#', ), ).toBe( - '

    Test
    \n' + - '#|
    \n' + - '||Cell in column 1, row 1
    \n' + + '

    Test
    \n' + + '#|
    \n' + + '||Cell in column 1, row 1
    \n' + '|Cell in column 2, row 1||

    \n' + - '

    ||Cell in column 1, row 2
    \n' + - '|Cell in column 2, row 2||
    \n' + + '

    ||Cell in column 1, row 2
    \n' + + '|Cell in column 2, row 2||
    \n' + '|#

    \n', ); }); diff --git a/test/term.test.ts b/test/term.test.ts index 6653365a..16685533 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -78,35 +78,10 @@ describe('Terms', () => {
    - - - - + + + + diff --git a/test/xss.test.ts b/test/xss.test.ts new file mode 100644 index 00000000..8453feed --- /dev/null +++ b/test/xss.test.ts @@ -0,0 +1,558 @@ +/* eslint no-useless-escape: 0 */ + +import transform from '../src/transform'; + +const transformYfm = (text: string, options?: Parameters[1]) => { + const { + result: {html}, + } = transform(text, { + allowHTML: true, + ...options, + }); + return html; +}; + +// https://sking7.github.io/articles/218647712.html + +describe('XSS Filter', () => { + it('XSS Locator', () => { + const input = `';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//-->">'>`; + const expected = `

    ';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//-->">'>

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('XSS locator 2', () => { + const input = `'';!--"=&{()}`; + const expected = `

    '';!--"=&

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('No Filter Evasion', () => { + const input = ``; + const expected = ''; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Image XSS using the JavaScript directive', () => { + const input = ``; + const expected = ''; + + expect(transformYfm(input)).toBe(expected); + }); + + it('No quotes and no semicolon', () => { + const input = ``; + const expected = `

    <img src=javascript:alert('XSS')>

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Case insensitive XSS attack vector', () => { + const input = ``; + const expected = `

    <img src=JaVaScRiPt:alert('XSS')>

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('HTML entities', () => { + const input = ``; + const expected = `

    <img src=javascript:alert("XSS")>

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Grave accent obfuscation', () => { + const input = ""; + const expected = `

    <img src='javascript:alert("RSnake says, 'XSS'")'>

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Malformed A tags', () => { + const input = 'xxs link'; + const expected = `

    xxs link

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Malformed img tags', () => { + const input = '">'; + const expected = `

    <img """>">

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('fromCharCode', () => { + const input = ''; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Default src tag to get past filters that check src domain', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Default src tag by leaving it empty', () => { + const input = ``; + const expected = `

    <img src= onmouseover="alert('xxs')">

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Default src tag by leaving it out entirely', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Decimal HTML character references', () => { + const input = ``; + const expected = `

    <img src=javascript:alert(
    \n'XSS')>

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Decimal HTML character references without trailing semicolons', () => { + const input = ``; + const expected = `

    <img src=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&
    \n#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Hexadecimal HTML character references without trailing semicolons', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Embedded tab', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Embedded Encoded tab', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Embedded newline to break up XSS', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Embedded carriage return to break up XSS', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Null breaks up JavaScript directive', () => { + const input = `perl -e 'print "";' > out`; + const expected = `

    perl -e 'print "<img src=java�script:alert("XSS")>";' > out

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Spaces and meta chars before the JavaScript in images for XSS', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Non-alpha-non-digit XSS 1', () => { + const input = ``; + const expected = `

    <script/xss src="http://ha.ckers.org/xss.js">

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Non-alpha-non-digit XSS 2', () => { + const input = ''; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Non-alpha-non-digit XSS 3', () => { + const input = ''; + const expected = `

    <script/src="http://ha.ckers.org/xss.js">

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Non-alpha-non-digit XSS 4', () => { + const input = '<'; + const expected = `

    <

    \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('No closing script tags', () => { + const input = '`; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('input image', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('body image', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('img Dynsrc', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('img lowsrc', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('List-style-image', () => { + const input = `
    • XSS
      `; + const expected = `
      • XSS
      `; + + expect(transformYfm(input)).toBe(expected); + }); + + it('VBscript in an image', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Livescript', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('body tag', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('BGSOUND', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('& JavaScript includes', () => { + const input = `
      `; + const expected = `
      `; + + expect(transformYfm(input)).toBe(expected); + }); + + it('style sheet', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Remote style sheet', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Remote style sheet part 2', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Remote style sheet part 3', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Remote style sheet part 4', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('style tags with broken up JavaScript for XSS', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('style tags with broken up JavaScript for XSS part 2', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('img style with expression', () => { + const input = `exp/*//*");

      \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('img style with expression', () => { + const input = `exp/*
      //*");

      \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('style tag', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('style tag using background-image', () => { + const input = `
      `; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('style tag using background', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Anonymous HTML with style attribute', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Local htc file', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('US-ASCII encoding', () => { + const input = `¼script¾alert(¢XSS¢)¼/script¾`; + const expected = `

      ¼script¾alert(¢XSS¢)¼/script¾

      \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('meta', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('iframe', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('iframe Event based', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('frame', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('TABLE', () => { + const input = ``; + const expected = `
      `; + + expect(transformYfm(input)).toBe(expected); + }); + + it('TD', () => { + const input = `
      `; + const expected = `
      `; + + expect(transformYfm(input)).toBe(expected); + }); + + it('DIV background-image', () => { + const input = `
      `; + const expected = `
      `; + + expect(transformYfm(input)).toBe(expected); + }); + + it('DIV background-image with unicoded XSS exploit', () => { + const input = `
      `; + const expected = `
      `; + + expect(transformYfm(input)).toBe(expected); + }); + + it('DIV background-image with unicoded XSS exploit 2', () => { + const input = `
      `; + const expected = `
      `; + + expect(transformYfm(input)).toBe(expected); + }); + + it('DIV expression', () => { + const input = `
      `; + const expected = `
      `; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Downlevel-Hidden block', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('BASE tag', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('OBJECT tag', () => { + const input = ``; + const expected = `

      \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Using an EMBED tag you can embed a Flash movie that contains XSS', () => { + const input = ``; + const expected = `

      <embed src="http://ha.ckers.Using an EMBED tag you can embed a Flash movie that contains XSS. Click here for a demo. If you add the attributes allowScriptAccess="never" and allownetworking="internal" it can mitigate this risk (thank you to Jonathan Vanasco for the info).:org/xss.swf" AllowScriptAccess="always">

      \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('You can EMBED SVG which can contain your XSS vector', () => { + const input = ``; + const expected = `

      \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('XML tag', () => { + const input = ``; + const expected = `

      \n`; + + expect(transformYfm(input)).toBe(expected); + }); + + it('Assuming you can only fit in a few characters and it filters against ".js"', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('SSI (Server Side Includes)', () => { + const input = ``; + const expected = ``; + + expect(transformYfm(input)).toBe(expected); + }); + + it('PHP', () => { + const input = `alert("XSS")'); ?>`; + const expected = `alert(\"XSS\")'); ?>`; + + expect(transformYfm(input)).toBe(expected); + }); +});