diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b9a2084 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + compatible-updates: + update-types: + - minor + - patch + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 2 diff --git a/SECURITY.md b/SECURITY.md index f173a10..378f5f0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,9 @@ # 安全与隐私 +## 依赖维护 + +使用 `npm ci` 安装锁定依赖,使用 `npm audit` 复查已知告警。`pptxgenjs` 的间接依赖 `image-size` 暂时覆盖为 `2.0.4`,以修复旧解析器处理部分图片格式时的无限循环问题。升级 `pptxgenjs` 后应复核是否仍需该覆盖;当前原生 PPTX 导出由 `test/pptxBuilder.test.js` 验证。 + ## 后台运营权限边界 - 组织后台的 `/api/ops/recent-errors` 只返回当前组织归属的系统事件和 AI 失败记录,不返回 `organization_id === null` 的平台级全局事件。 diff --git a/build/bundle.js b/build/bundle.js index df4a9c3..8295f51 100644 --- a/build/bundle.js +++ b/build/bundle.js @@ -11242,11 +11242,17 @@ */ XMLNS: "http://www.w3.org/2000/xmlns/" }); + var nameStartChar = /[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/; + var nameChar = new RegExp("[\\-\\.0-9" + nameStartChar.source.slice(1, -1) + "\\u00B7\\u0300-\\u036F\\u203F-\\u2040]"); + var tagNamePattern = new RegExp("^" + nameStartChar.source + nameChar.source + "*(?::" + nameStartChar.source + nameChar.source + "*)?$"); exports2.assign = assign; exports2.find = find2; exports2.freeze = freeze; exports2.MIME_TYPE = MIME_TYPE; exports2.NAMESPACE = NAMESPACE; + exports2.nameStartChar = nameStartChar; + exports2.nameChar = nameChar; + exports2.tagNamePattern = tagNamePattern; } }); @@ -11256,6 +11262,7 @@ var conventions = require_conventions(); var find2 = conventions.find; var NAMESPACE = conventions.NAMESPACE; + var tagNamePattern = conventions.tagNamePattern; function notEmptyString(input) { return input !== ""; } @@ -11419,6 +11426,7 @@ }; _extends(LiveNodeList, NodeList); function NamedNodeMap() { + this._nameIndex = /* @__PURE__ */ Object.create(null); } function _findNodeIndex(list, node) { var i = list.length; @@ -11428,12 +11436,22 @@ } } } + function _nnmIndexAdd(list, attr) { + list._nameIndex[attr.nodeName] = attr; + } + function _nnmIndexRemove(list, attr) { + if (list._nameIndex[attr.nodeName] === attr) { + delete list._nameIndex[attr.nodeName]; + } + } function _addNamedNode(el, list, newAttr, oldAttr) { if (oldAttr) { list[_findNodeIndex(list, oldAttr)] = newAttr; + _nnmIndexRemove(list, oldAttr); } else { list[list.length++] = newAttr; } + _nnmIndexAdd(list, newAttr); if (el) { newAttr.ownerElement = el; var doc = el.ownerDocument; @@ -11451,6 +11469,7 @@ list[i] = list[++i]; } list.length = lastIndex; + _nnmIndexRemove(list, attr); if (el) { var doc = el.ownerDocument; if (doc) { @@ -11479,7 +11498,7 @@ if (el && el != this._ownerElement) { throw new DOMException(INUSE_ATTRIBUTE_ERR); } - var oldAttr = this.getNamedItem(attr.nodeName); + var oldAttr = this._nameIndex[attr.nodeName]; _addNamedNode(this._ownerElement, this, attr, oldAttr); return oldAttr; }, @@ -11682,8 +11701,29 @@ while (child) { var next = child.nextSibling; if (next !== null && next.nodeType === TEXT_NODE && child.nodeType === TEXT_NODE) { - node.removeChild(next); - child.appendData(next.data); + var tail = []; + var sibling = next; + while (sibling !== null && sibling.nodeType === TEXT_NODE) { + tail.push(sibling.data); + sibling = sibling.nextSibling; + } + var removed = child.nextSibling; + while (removed !== sibling) { + var following = removed.nextSibling; + removed.parentNode = null; + removed.previousSibling = null; + removed.nextSibling = null; + removed = following; + } + child.nextSibling = sibling; + if (sibling !== null) { + sibling.previousSibling = child; + } else { + node.lastChild = child; + } + child.appendData(tail.join("")); + _onUpdateChild(node.ownerDocument, node); + child = sibling; } else { child = next; } @@ -12198,8 +12238,10 @@ * - it does not do any input validation on the arguments and doesn't throw "InvalidCharacterError". * * Note: When the resulting document is serialized with `requireWellFormed: true`, the - * serializer throws with code `INVALID_STATE_ERR` if `.data` contains `?>` (W3C DOM Parsing - * §3.2.1.7). Without that option the data is emitted verbatim. + * serializer throws with code `INVALID_STATE_ERR` if `.target` is not a valid XML `NCName` + * (a `Name` with no colon) or is an ASCII case-insensitive match for `"xml"`, or if `.data` + * contains `?>` (W3C DOM Parsing §3.2.1.7). Without that option the target and data are + * emitted verbatim. * * @param {string} target * @param {string} data @@ -12224,7 +12266,27 @@ node.specified = true; return node; }, + /** + * Creates an EntityReference object, serialized as `&name;`. + * + * The `name` is validated against the XML `Name` production at creation time; an invalid name + * throws a `DOMException` with code `INVALID_CHARACTER_ERR`. When the resulting node is + * serialized with `requireWellFormed: true`, the serializer re-validates `nodeName` and throws + * a `DOMException` with code `INVALID_STATE_ERR` if a later `nodeName` mutation made it invalid; + * without that option the name is emitted verbatim. + * + * Note: xmldom does not expand entities — the parser resolves entity references inline and never + * constructs `EntityReference` nodes, so this method is the only producer. + * + * @param {string} name The name of the entity to reference. + * @returns {EntityReference} + * @throws {DOMException} With code `INVALID_CHARACTER_ERR` when `name` is not a valid XML `Name`. + * @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-392B75AE + */ createEntityReference: function(name) { + if (!tagNamePattern.test(name)) { + throw new DOMException(INVALID_CHARACTER_ERR, 'not a valid xml name "' + name + '"'); + } var node = new EntityReference(); node.ownerDocument = this; node.nodeName = name; @@ -12489,7 +12551,10 @@ } return true; } - function addSerializedAttribute(buf, qualifiedName, value) { + function addSerializedAttribute(buf, qualifiedName, value, requireWellFormed) { + if (requireWellFormed && !tagNamePattern.test(qualifiedName)) { + throw new DOMException(INVALID_STATE_ERR, 'The attribute name "' + qualifiedName + '" is not a valid XML QName'); + } buf.push(" ", qualifiedName, '="', value.replace(/[<>&"\t\n\r]/g, _xmlEncoder), '"'); } function serializeToString(node, buf, isHTML, nodeFilter, visibleNamespaces, requireWellFormed) { @@ -12547,6 +12612,9 @@ } } } + if (requireWellFormed && !tagNamePattern.test(prefixedNodeName)) { + throw new DOMException(INVALID_STATE_ERR, 'The element name "' + prefixedNodeName + '" is not a valid XML QName'); + } buf.push("<", prefixedNodeName); var childNs = ns.slice(); for (var i = 0; i < len; i++) { @@ -12562,7 +12630,7 @@ if (needNamespaceDefine(attr, html, childNs)) { var attrPrefix = attr.prefix || ""; var uri = attr.namespaceURI; - addSerializedAttribute(buf, attrPrefix ? "xmlns:" + attrPrefix : "xmlns", uri); + addSerializedAttribute(buf, attrPrefix ? "xmlns:" + attrPrefix : "xmlns", uri, requireWellFormed); childNs.push({ prefix: attrPrefix, namespace: uri }); } var filteredAttr = nodeFilter ? nodeFilter(attr) : attr; @@ -12570,14 +12638,14 @@ if (typeof filteredAttr === "string") { buf.push(filteredAttr); } else { - addSerializedAttribute(buf, filteredAttr.name, filteredAttr.value); + addSerializedAttribute(buf, filteredAttr.name, filteredAttr.value, requireWellFormed); } } } if (nodeName === prefixedNodeName && needNamespaceDefine(n, html, childNs)) { var nodePrefix = n.prefix || ""; var uri = n.namespaceURI; - addSerializedAttribute(buf, nodePrefix ? "xmlns:" + nodePrefix : "xmlns", uri); + addSerializedAttribute(buf, nodePrefix ? "xmlns:" + nodePrefix : "xmlns", uri, requireWellFormed); childNs.push({ prefix: nodePrefix, namespace: uri }); } var child = n.firstChild; @@ -12604,7 +12672,7 @@ case DOCUMENT_FRAGMENT_NODE: return { ns: ns.slice(), isHTML: html, tag: null }; case ATTRIBUTE_NODE: - addSerializedAttribute(buf, n.name, n.value); + addSerializedAttribute(buf, n.name, n.value, requireWellFormed); return null; case TEXT_NODE: buf.push(n.data.replace(/[<&>]/g, _xmlEncoder)); @@ -12623,6 +12691,9 @@ return null; case DOCUMENT_TYPE_NODE: if (requireWellFormed) { + if (!tagNamePattern.test(n.name)) { + throw new DOMException(INVALID_STATE_ERR, 'The doctype name "' + n.name + '" is not a valid XML Name'); + } if (n.publicId && !/^("[\x20\r\na-zA-Z0-9\-()+,.\/:=?;!*#@$_%']*"|'[\x20\r\na-zA-Z0-9\-()+,.\/:=?;!*#@$_%'"]*')$/.test(n.publicId)) { throw new DOMException(INVALID_STATE_ERR, "DocumentType publicId is not a valid PubidLiteral"); } @@ -12653,12 +12724,26 @@ } return null; case PROCESSING_INSTRUCTION_NODE: - if (requireWellFormed && n.data.indexOf("?>") !== -1) { - throw new DOMException(INVALID_STATE_ERR, 'The ProcessingInstruction data contains "?>"'); + if (requireWellFormed) { + if (!tagNamePattern.test(n.target) || n.target.indexOf(":") !== -1 || n.target.toLowerCase() === "xml") { + throw new DOMException( + INVALID_STATE_ERR, + 'The processing instruction target "' + n.target + '" is not a valid XML NCName or is reserved' + ); + } + if (n.data.indexOf("?>") !== -1) { + throw new DOMException(INVALID_STATE_ERR, 'The ProcessingInstruction data contains "?>"'); + } } buf.push(""); return null; case ENTITY_REFERENCE_NODE: + if (requireWellFormed && !tagNamePattern.test(n.nodeName)) { + throw new DOMException( + INVALID_STATE_ERR, + 'The entity reference name "' + n.nodeName + '" is not a valid XML Name' + ); + } buf.push("&", n.nodeName, ";"); return null; //case ENTITY_NODE: @@ -14948,9 +15033,7 @@ var require_sax = __commonJS({ "node_modules/@xmldom/xmldom/lib/sax.js"(exports2) { var NAMESPACE = require_conventions().NAMESPACE; - var nameStartChar = /[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/; - var nameChar = new RegExp("[\\-\\.0-9" + nameStartChar.source.slice(1, -1) + "\\u00B7\\u0300-\\u036F\\u203F-\\u2040]"); - var tagNamePattern = new RegExp("^" + nameStartChar.source + nameChar.source + "*(?::" + nameStartChar.source + nameChar.source + "*)?$"); + var tagNamePattern = require_conventions().tagNamePattern; var S_TAG = 0; var S_ATTR = 1; var S_ATTR_SPACE = 2; @@ -15045,7 +15128,7 @@ switch (source.charAt(tagStart + 1)) { case "/": var end = source.indexOf(">", tagStart + 3); - var tagName = source.substring(tagStart + 2, end).replace(/[ \t\n\r]+$/g, ""); + var tagName = source.substring(tagStart + 2, end).replace(/^([\s\S]*?[^ \t\n\r])?[ \t\n\r]*$/, "$1"); var config = parseStack.pop(); if (end < 0) { tagName = source.substring(tagStart + 2).replace(/[\s<].*/, ""); @@ -15055,6 +15138,8 @@ tagName = tagName.replace(/[\s<].*/, ""); errorHandler.error("end tag name: " + tagName + " maybe not complete"); end = tagStart + 1 + tagName.length; + } else if (/[ \t\n\r]/.test(tagName) && tagNamePattern.test(tagName.split(/[ \t\n\r]/)[0])) { + errorHandler.error('end tag name is followed by whitespace and trailing content: "' + tagName + '"'); } var localNSMap = config.localNSMap; var endMatch = config.tagName == tagName; @@ -15160,6 +15245,9 @@ var s = S_TAG; while (true) { var c = source.charAt(p); + if (s === S_TAG && c === "<") { + throw new Error("unexpected < in tag name: " + source.slice(start, p)); + } switch (c) { case "=": if (s === S_ATTR) { @@ -15334,7 +15422,7 @@ if (nsPrefix !== false) { if (localNSMap == null) { localNSMap = {}; - _copy(currentNSMap, currentNSMap = {}); + currentNSMap = Object.create(currentNSMap); } currentNSMap[nsPrefix] = localNSMap[nsPrefix] = value; a.uri = NAMESPACE.XMLNS; diff --git a/package-lock.json b/package-lock.json index 6c3cf31..b0ac538 100644 --- a/package-lock.json +++ b/package-lock.json @@ -487,9 +487,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -615,18 +615,15 @@ "license": "ISC" }, "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.4.tgz", + "integrity": "sha512-QRUkFFsRV/6fuESxb9Vkq+a0LkSrgKXuc2NEqfikiXxxN/G3tjWt5EVUlMaImRBZRZK/jRBEbYvpPYZL8t08Zw==", "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, "bin": { "image-size": "bin/image-size.js" }, "engines": { - "node": ">=16.x" + "node": ">=18" } }, "node_modules/immediate": { @@ -902,15 +899,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", diff --git a/package.json b/package.json index 9f92dbf..1ff9c40 100644 --- a/package.json +++ b/package.json @@ -31,5 +31,10 @@ "mammoth": "^1.12.0", "pg": "^8.13.1", "pptxgenjs": "^4.0.1" + }, + "overrides": { + "pptxgenjs": { + "image-size": "2.0.4" + } } }