🔒 Fixed SVG sanitization for staff profile pictures (#21798)
closes https://linear.app/ghost/issue/ENG-1506 - when uploading a SVG image as staff profile picture, we previously had a validation against malicious `<script>` tags or `on*` attributes - this has proven to be unsufficient, as malicious scripts can be added via other tags (e.g. `<foreignObject>`) and other attributes (e.g. `xlink:href`) - we now satinize SVGs using the DOMPurify library during validation - if the file is invalid and cannot be sanitized, we show an error to the user - also added support for sanitizing `.svgz` files
|
@ -2,11 +2,16 @@ const path = require('path');
|
|||
const os = require('os');
|
||||
const multer = require('multer');
|
||||
const fs = require('fs-extra');
|
||||
const zlib = require('zlib');
|
||||
const util = require('util');
|
||||
const errors = require('@tryghost/errors');
|
||||
const config = require('../../../../shared/config');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
const logging = require('@tryghost/logging');
|
||||
|
||||
const gunzip = util.promisify(zlib.gunzip);
|
||||
const gzip = util.promisify(zlib.gzip);
|
||||
|
||||
const messages = {
|
||||
db: {
|
||||
missingFile: 'Please select a database file to import.',
|
||||
|
@ -32,6 +37,10 @@ const messages = {
|
|||
missingFile: 'Please select an image.',
|
||||
invalidFile: 'Please select a valid image.'
|
||||
},
|
||||
svg: {
|
||||
missingFile: 'Please select a SVG image.',
|
||||
invalidFile: 'Please select a valid SVG image'
|
||||
},
|
||||
icons: {
|
||||
missingFile: 'Please select an icon.',
|
||||
invalidFile: 'Icon must be a square .ico or .png file between 60px – 1,000px, under 100kb.'
|
||||
|
@ -144,39 +153,99 @@ const checkFileExists = (fileData) => {
|
|||
|
||||
const checkFileIsValid = (fileData, types, extensions) => {
|
||||
const type = fileData.mimetype;
|
||||
|
||||
if (types.includes(type) && extensions.includes(fileData.ext)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} filepath
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* Checks for the presence of <script> tags or 'on' attributes in an SVG file
|
||||
* @returns {String | null}
|
||||
*
|
||||
* Reads the SVG file, sanitizes it, and writes the sanitized content back to the file.
|
||||
* Returns the sanitized content or null if the SVG could not be sanitized.
|
||||
*/
|
||||
const isSvgSafe = (filepath) => {
|
||||
const {JSDOM} = require('jsdom');
|
||||
|
||||
const fileContent = fs.readFileSync(filepath, 'utf8');
|
||||
const document = new JSDOM(fileContent).window.document;
|
||||
document.body.innerHTML = fileContent;
|
||||
const svgEl = document.body.firstElementChild;
|
||||
const sanitizeSvg = async (filepath, isZipped = false) => {
|
||||
try {
|
||||
const original = await readSvg(filepath, isZipped);
|
||||
const sanitized = sanitizeSvgContent(original);
|
||||
|
||||
if (!svgEl) {
|
||||
return false;
|
||||
if (!sanitized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await writeSvg(filepath, sanitized, isZipped);
|
||||
return sanitized;
|
||||
} catch (error) {
|
||||
logging.error('Error sanitizing SVG:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const attributes = Array.from(svgEl.attributes).map(({name}) => name);
|
||||
const hasScriptAttr = !!attributes.find(attr => attr.startsWith('on'));
|
||||
const scripts = svgEl.getElementsByTagName('script');
|
||||
|
||||
return scripts.length === 0 && !hasScriptAttr ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} content
|
||||
* @returns {String | null}
|
||||
*
|
||||
* Returns sanitized SVG content, or null if the content is invalid.
|
||||
*
|
||||
*/
|
||||
const sanitizeSvgContent = (content) => {
|
||||
const {JSDOM} = require('jsdom');
|
||||
const createDOMPurify = require('dompurify');
|
||||
const window = new JSDOM('').window;
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
|
||||
const sanitized = DOMPurify.sanitize(content, {USE_PROFILES: {svg: true, svgFilters: true}});
|
||||
|
||||
// Check whether the sanitized content still contains a non-empty <svg> tag
|
||||
const validSvgTag = sanitized?.match(/<svg[^>]*>\s*[\S]+[\S\s]*<\/svg>/);
|
||||
if (!sanitized || sanitized.trim() === '' || !validSvgTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} filepath
|
||||
* @param {Boolean} isZipped
|
||||
* @returns {String | null}
|
||||
*
|
||||
* Reads .svg or .svgz files and returns the content as a string.
|
||||
*
|
||||
*/
|
||||
const readSvg = async (filepath, isZipped = false) => {
|
||||
if (isZipped) {
|
||||
const compressed = await fs.readFile(filepath);
|
||||
return (await gunzip(compressed)).toString();
|
||||
}
|
||||
|
||||
return await fs.readFile(filepath, 'utf8');
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} filepath
|
||||
* @param {String} content
|
||||
* @param {Boolean} isZipped
|
||||
*
|
||||
* Writes SVG content to a .svg or .svgz file.
|
||||
*/
|
||||
const writeSvg = async (filepath, content, isZipped = false) => {
|
||||
if (isZipped) {
|
||||
const compressed = await gzip(content);
|
||||
return await fs.writeFile(filepath, compressed);
|
||||
}
|
||||
|
||||
return await fs.writeFile(filepath, content);
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @param {Object} options
|
||||
|
@ -191,7 +260,7 @@ const validation = function ({type}) {
|
|||
* @param {import('express').Response} res
|
||||
* @param {import('express').NextFunction} next
|
||||
*/
|
||||
return function uploadValidation(req, res, next) {
|
||||
return async function uploadValidation(req, res, next) {
|
||||
const extensions = (config.get('uploads')[type] && config.get('uploads')[type].extensions) || [];
|
||||
const contentTypes = (config.get('uploads')[type] && config.get('uploads')[type].contentTypes) || [];
|
||||
|
||||
|
@ -215,10 +284,13 @@ const validation = function ({type}) {
|
|||
}));
|
||||
}
|
||||
|
||||
if (req.file.ext === '.svg') {
|
||||
if (!isSvgSafe(req.file.path)) {
|
||||
// Sanitize SVG files
|
||||
if (req.file.ext === '.svg' || req.file.ext === '.svgz') {
|
||||
const sanitized = await sanitizeSvg(req.file.path, req.file.ext === '.svgz');
|
||||
|
||||
if (!sanitized) {
|
||||
return next(new errors.UnsupportedMediaTypeError({
|
||||
message: 'SVG files cannot contain <script> tags or "on" attributes.'
|
||||
message: tpl(messages.svg.invalidFile)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -295,5 +367,5 @@ module.exports = {
|
|||
module.exports._test = {
|
||||
checkFileExists,
|
||||
checkFileIsValid,
|
||||
isSvgSafe
|
||||
sanitizeSvgContent
|
||||
};
|
||||
|
|
|
@ -179,6 +179,7 @@
|
|||
"connect-slashes": "1.4.0",
|
||||
"cookie-session": "2.1.0",
|
||||
"cors": "2.8.5",
|
||||
"dompurify": "^3.2.2",
|
||||
"downsize": "0.0.8",
|
||||
"express": "4.21.1",
|
||||
"express-brute": "1.0.1",
|
||||
|
|
|
@ -162,6 +162,42 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Images API Errors when uploading an invalid SVG 1: [body] 1`] = `
|
||||
Object {
|
||||
"errors": Array [
|
||||
Object {
|
||||
"code": null,
|
||||
"context": null,
|
||||
"details": null,
|
||||
"ghostErrorCode": null,
|
||||
"help": null,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
"message": "Please select a valid SVG image",
|
||||
"property": null,
|
||||
"type": "UnsupportedMediaTypeError",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Images API Errors when uploading an invalid zipped SVG 1: [body] 1`] = `
|
||||
Object {
|
||||
"errors": Array [
|
||||
Object {
|
||||
"code": null,
|
||||
"context": null,
|
||||
"details": null,
|
||||
"ghostErrorCode": null,
|
||||
"help": null,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
"message": "Please select a valid SVG image",
|
||||
"property": null,
|
||||
"type": "UnsupportedMediaTypeError",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Images API Will error when filename is too long 1: [body] 1`] = `
|
||||
Object {
|
||||
"errors": Array [
|
||||
|
|
|
@ -72,8 +72,23 @@ const uploadImageCheck = async ({path, filename, contentType, expectedFileName,
|
|||
|
||||
// Check the image is saved to disk
|
||||
const saved = await fs.readFile(originalFilePath);
|
||||
assert.equal(saved.length, fileContents.length);
|
||||
assert.deepEqual(saved, fileContents);
|
||||
|
||||
// Check the content of the saved image:
|
||||
// - SVGs are sanitized before save, so their content is smaller or equal than the original file
|
||||
// - Non-SVGs are saved as-is, so their content should be the same as the original file
|
||||
if (contentType.includes('svg')) {
|
||||
assert.ok(saved.length <= fileContents.length);
|
||||
assert.ok(!saved.includes('<script'));
|
||||
assert.ok(!saved.includes('<foreignObject'));
|
||||
assert.ok(!saved.includes('<iframe'));
|
||||
assert.ok(!saved.includes('<embed'));
|
||||
assert.ok(!saved.includes('onclick'));
|
||||
assert.ok(!saved.includes('href'));
|
||||
assert.ok(!saved.includes('xlink:href'));
|
||||
} else {
|
||||
assert.equal(saved.length, fileContents.length);
|
||||
assert.deepEqual(saved, fileContents);
|
||||
}
|
||||
|
||||
const savedResized = await fs.readFile(filePath);
|
||||
assert.ok(savedResized.length <= fileContents.length); // should always be smaller
|
||||
|
@ -176,9 +191,50 @@ describe('Images API', function () {
|
|||
await uploadImageCheck({path: originalFilePath, filename: 'ghosticon.webp', contentType: 'image/webp'});
|
||||
});
|
||||
|
||||
it('Can upload a svg', async function () {
|
||||
it('Can upload a valid svg', async function () {
|
||||
const originalFilePath = p.join(__dirname, '/../../utils/fixtures/images/ghost-logo.svg');
|
||||
await uploadImageCheck({path: originalFilePath, filename: 'ghost.svg', contentType: 'image/svg+xml', skipOriginal: true});
|
||||
await uploadImageCheck({path: originalFilePath, filename: 'ghost-logo.svg', contentType: 'image/svg+xml', skipOriginal: true});
|
||||
});
|
||||
|
||||
it('Can upload a svg that needs sanitization', async function () {
|
||||
const originalFilePath = p.join(__dirname, '/../../utils/fixtures/images/svg-with-unsafe-script.svg');
|
||||
await uploadImageCheck({path: originalFilePath, filename: 'svg-with-unsafe-script.svg', contentType: 'image/svg+xml', skipOriginal: true});
|
||||
});
|
||||
|
||||
it('Errors when uploading an invalid SVG', async function () {
|
||||
const originalFilePath = p.join(__dirname, '/../../utils/fixtures/images/svg-malformed.svg');
|
||||
const fileContents = await fs.readFile(originalFilePath);
|
||||
await uploadImageRequest({fileContents, filename: 'svg-malformed.svg', contentType: 'image/svg+xml'})
|
||||
.expectStatus(415)
|
||||
.matchBodySnapshot({
|
||||
errors: [{
|
||||
id: anyErrorId,
|
||||
message: 'Please select a valid SVG image'
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('Can upload a valid zipped SVG', async function () {
|
||||
const originalFilePath = p.join(__dirname, '/../../utils/fixtures/images/ghost-logo.svgz');
|
||||
await uploadImageCheck({path: originalFilePath, filename: 'ghost-logo.svgz', contentType: 'image/svg+xml', skipOriginal: true});
|
||||
});
|
||||
|
||||
it('Can upload a zipped svg that needs sanitization', async function () {
|
||||
const originalFilePath = p.join(__dirname, '/../../utils/fixtures/images/svgz-with-unsafe-script.svgz');
|
||||
await uploadImageCheck({path: originalFilePath, filename: 'svg-with-unsafe-script.svgz', contentType: 'image/svg+xml', skipOriginal: true});
|
||||
});
|
||||
|
||||
it('Errors when uploading an invalid zipped SVG', async function () {
|
||||
const originalFilePath = p.join(__dirname, '/../../utils/fixtures/images/svgz-malformed.svgz');
|
||||
const fileContents = await fs.readFile(originalFilePath);
|
||||
await uploadImageRequest({fileContents, filename: 'svgz-malformed.svgz', contentType: 'image/svg+xml'})
|
||||
.expectStatus(415)
|
||||
.matchBodySnapshot({
|
||||
errors: [{
|
||||
id: anyErrorId,
|
||||
message: 'Please select a valid SVG image'
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('Can upload a square profile image', async function () {
|
||||
|
|
|
@ -3,6 +3,7 @@ const validation = require('../../../../../../core/server/web/api/middleware/upl
|
|||
const imageFixturePath = ('../../../../../utils/fixtures/images/');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert/strict');
|
||||
|
||||
describe('web utils', function () {
|
||||
describe('checkFileExists', function () {
|
||||
|
@ -44,29 +45,84 @@ describe('web utils', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('isSvgSafe', function () {
|
||||
it('detects a <script> tag in a svg file', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'svg-with-script.svg');
|
||||
const dirtySvgContent = fs.readFileSync(filepath, 'utf8');
|
||||
dirtySvgContent.should.containEql('<script');
|
||||
validation.isSvgSafe(filepath).should.be.false;
|
||||
});
|
||||
it('detects a on attribute in a svg file', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'svg-with-script2.svg');
|
||||
const dirtySvgContent = fs.readFileSync(filepath, 'utf8');
|
||||
dirtySvgContent.should.containEql('onclick');
|
||||
validation.isSvgSafe(filepath).should.be.false;
|
||||
});
|
||||
it('returns true for a safe svg file', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'ghost-logo.svg');
|
||||
const dirtySvgContent = fs.readFileSync(filepath, 'utf8');
|
||||
dirtySvgContent.should.not.containEql('<script');
|
||||
validation.isSvgSafe(filepath).should.be.true;
|
||||
describe('sanitizeSvgContent', function () {
|
||||
it('it removes <script> tags from SVGs', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'svg-with-unsafe-script.svg');
|
||||
const original = fs.readFileSync(filepath, 'utf8');
|
||||
|
||||
assert.ok(original.includes('<script'));
|
||||
|
||||
const sanitized = validation.sanitizeSvgContent(original);
|
||||
assert.ok(!sanitized.includes('<script'), 'Sanitized SVG should not contain a <script> tag');
|
||||
});
|
||||
|
||||
it('returns false for malformed svg', async function () {
|
||||
it('it removes <foreignObject> tags from SVGs', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'svg-with-unsafe-foreign-object.svg');
|
||||
const original = fs.readFileSync(filepath, 'utf8');
|
||||
|
||||
assert.ok(original.includes('<foreignObject'));
|
||||
|
||||
const sanitized = validation.sanitizeSvgContent(original);
|
||||
assert.ok(!sanitized.includes('<foreignObject'), 'Sanitized SVG should not contain a <foreignObject> tag');
|
||||
});
|
||||
|
||||
it('it removes <embed> tags from SVGs', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'svg-with-unsafe-embed.svg');
|
||||
const original = fs.readFileSync(filepath, 'utf8');
|
||||
|
||||
assert.ok(original.includes('<embed'));
|
||||
|
||||
const sanitized = validation.sanitizeSvgContent(original);
|
||||
assert.ok(!sanitized.includes('<embed'), 'Sanitized SVG should not contain a <embed> tag');
|
||||
});
|
||||
|
||||
it('it removes on* attributes from SVGs', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'svg-with-unsafe-onclick.svg');
|
||||
const original = fs.readFileSync(filepath, 'utf8');
|
||||
|
||||
assert.ok(original.includes('onclick'));
|
||||
|
||||
const sanitized = validation.sanitizeSvgContent(original);
|
||||
assert.ok(!sanitized.includes('onclick'), 'Sanitized SVG should not contain an onclick attribute');
|
||||
});
|
||||
|
||||
it('it removes href attributes from SVGs', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'svg-with-unsafe-href.svg');
|
||||
const original = fs.readFileSync(filepath, 'utf8');
|
||||
|
||||
assert.ok(original.includes('href'));
|
||||
|
||||
const sanitized = validation.sanitizeSvgContent(original);
|
||||
assert.ok(!sanitized.includes('href'), 'Sanitized SVG should not contain an href attribute');
|
||||
});
|
||||
|
||||
it('it removes xlink:href attributes from SVGs', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'svg-with-unsafe-xlink-href.svg');
|
||||
const original = fs.readFileSync(filepath, 'utf8');
|
||||
|
||||
assert.ok(original.includes('xlink:href'));
|
||||
|
||||
const sanitized = validation.sanitizeSvgContent(original);
|
||||
assert.ok(!sanitized.includes('xlink:href'), 'Sanitized SVG should not contain an xlink:href attribute');
|
||||
});
|
||||
|
||||
it('it returns null for malformed SVGs', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'svg-malformed.svg');
|
||||
validation.isSvgSafe(filepath).should.be.false;
|
||||
const original = fs.readFileSync(filepath, 'utf8');
|
||||
|
||||
const sanitized = validation.sanitizeSvgContent(original);
|
||||
assert.equal(sanitized, null, 'Malformed SVG should return null after sanitization');
|
||||
});
|
||||
|
||||
it('returns true for a safe svg file', async function () {
|
||||
const filepath = path.join(__dirname, imageFixturePath, 'ghost-logo.svg');
|
||||
const original = fs.readFileSync(filepath, 'utf8');
|
||||
|
||||
assert.ok(!original.includes('<script'));
|
||||
assert.ok(!original.includes('onclick'));
|
||||
|
||||
const sanitized = validation.sanitizeSvgContent(original);
|
||||
assert.ok(sanitized, 'Safe SVG should return a string after sanitization');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
BIN
ghost/core/test/utils/fixtures/images/ghost-logo.svgz
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
|
||||
<embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnVW5zYWZlIGVtYmVkJyk7PC9zY3JpcHQ+" />
|
||||
</svg>
|
After Width: | Height: | Size: 265 B |
|
@ -0,0 +1,6 @@
|
|||
<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<text x="20" y="35">test</text>
|
||||
<foreignObject width="500" height="500">
|
||||
<iframe xmlns="http://www.w3.org/1999/xhtml" src="javascript:confirm(document.domain);" width="400" height="250"/>
|
||||
</foreignObject>
|
||||
</svg>
|
After Width: | Height: | Size: 341 B |
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<a href="javascript:alert('Unsafe href!');">
|
||||
<text x="10" y="50" font-size="20">Unsafe Link</text>
|
||||
</a>
|
||||
</svg>
|
After Width: | Height: | Size: 193 B |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<image href="javascript:alert('Unsafe image!');" width="100" height="100" />
|
||||
</svg>
|
After Width: | Height: | Size: 154 B |
|
@ -3,4 +3,4 @@
|
|||
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
|
||||
<button onclick="alert(1)">Click me</button>
|
||||
</svg>
|
||||
</svg>
|
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 362 B |
|
@ -5,4 +5,4 @@
|
|||
<script type="text/javascript">
|
||||
alert(1);
|
||||
</script>
|
||||
</svg>
|
||||
</svg>
|
Before Width: | Height: | Size: 377 B After Width: | Height: | Size: 378 B |
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100">
|
||||
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
|
||||
<use xlink:href="javascript:alert('Unsafe xlink:href!');" />
|
||||
</svg>
|
After Width: | Height: | Size: 277 B |
BIN
ghost/core/test/utils/fixtures/images/svgz-malformed.svgz
Normal file
42
yarn.lock
|
@ -8528,6 +8528,11 @@
|
|||
dependencies:
|
||||
"@types/jest" "*"
|
||||
|
||||
"@types/trusted-types@^2.0.7":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||
|
||||
"@types/unist@^2.0.0", "@types/unist@^2.0.2":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||
|
@ -14441,6 +14446,13 @@ domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3:
|
|||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
dompurify@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.2.tgz#6c0518745e81686c74a684f5af1e5613e7cc0246"
|
||||
integrity sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
domutils@1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
|
||||
|
@ -19241,7 +19253,17 @@ htmlparser2@^6.1.0:
|
|||
domutils "^2.5.2"
|
||||
entities "^2.0.0"
|
||||
|
||||
htmlparser2@^8.0.0, htmlparser2@^8.0.1:
|
||||
htmlparser2@^8.0.0:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
|
||||
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
entities "^4.4.0"
|
||||
|
||||
htmlparser2@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010"
|
||||
integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==
|
||||
|
@ -25230,7 +25252,7 @@ picocolors@^0.2.1:
|
|||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
|
||||
integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==
|
||||
|
||||
picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0:
|
||||
picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0, picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
@ -26250,7 +26272,7 @@ postcss-values-parser@^4.0.0:
|
|||
is-url-superb "^4.0.0"
|
||||
postcss "^7.0.5"
|
||||
|
||||
postcss@8.4.39, postcss@^8.1.4, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.3.11, postcss@^8.4.19, postcss@^8.4.23, postcss@^8.4.27, postcss@^8.4.4:
|
||||
postcss@8.4.39, postcss@^8.1.4, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.4.19, postcss@^8.4.23, postcss@^8.4.27, postcss@^8.4.4:
|
||||
version "8.4.39"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3"
|
||||
integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==
|
||||
|
@ -26267,6 +26289,15 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2
|
|||
picocolors "^0.2.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
postcss@^8.3.11:
|
||||
version "8.4.49"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19"
|
||||
integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
prebuild-install@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
|
||||
|
@ -28821,6 +28852,11 @@ source-map-js@^1.0.1, source-map-js@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
||||
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
|
||||
|
||||
source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
source-map-resolve@^0.5.0:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
|
||||
|
|