From d6f9a7b6b6a1ed64086ea0871a5c801c71338c8b Mon Sep 17 00:00:00 2001
From: Blue Daydreaming <26271654+blueDaydreaming725@users.noreply.github.com>
Date: Mon, 9 Nov 2026 11:57:10 +0800
Subject: [PATCH] fix: reject invalid content after the root element
ref:0
---
spec/validator_spec.js | 27 ++++++++++++++++++++++++++-
src/validator.js | 12 ++++++++++++
2 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/spec/validator_spec.js b/spec/validator_spec.js
index 0d07e227..e6d91292 100644
--- a/spec/validator_spec.js
+++ b/spec/validator_spec.js
@@ -2,7 +2,7 @@
import fs from "fs";
import path from "path";
-import {XMLValidator} from "../src/fxp.js";
+import {XMLParser, XMLValidator} from "../src/fxp.js";
import { fileURLToPath } from 'url';
import { dirname } from 'path';
@@ -405,6 +405,31 @@ describe("should not validate XML documents with multiple root nodes", () => {
});
});
+describe("should validate only allowed content after the root element", () => {
+ const invalidContent = ['&', 'A', '', ''];
+ const roots = ['', ''];
+
+ it('rejects references, CDATA and DOCTYPE in the epilog', () => {
+ for (const root of roots) for (const content of invalidContent) {
+ expect(XMLValidator.validate(`${root}${content}`)).not.toBe(true);
+ }
+ expect(XMLValidator.validate('')).not.toBe(true);
+ });
+
+ it('rejects the same content when parsing with validation enabled', () => {
+ const parser = new XMLParser();
+ for (const root of roots) for (const content of invalidContent) {
+ expect(() => parser.parse(`${root}${content}`, true)).toThrow();
+ }
+ });
+
+ it('allows whitespace, comments and processing instructions in the epilog', () => {
+ for (const root of roots) {
+ validate(`${root} \n`);
+ }
+ });
+});
+
describe("should report correct line numbers for unclosed tags", () => {
it('- child tag', () =>
diff --git a/src/validator.js b/src/validator.js
index 6f9904f0..1a2be6c3 100644
--- a/src/validator.js
+++ b/src/validator.js
@@ -85,12 +85,18 @@ export function validate(xmlData, options) {
i = result.index;
if (attrStr[attrStr.length - 1] === '/') {
+ if (reachedRoot === true) {
+ return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i));
+ }
//self closing tag
const attrStrStart = i - attrStr.length;
attrStr = attrStr.substring(0, attrStr.length - 1);
const isValid = validateAttributeString(attrStr, options);
if (isValid === true) {
tagFound = true;
+ if (tags.length === 0) {
+ reachedRoot = true;
+ }
//continue; //text may presents after self closing tag
} else {
//the result from the nested function returns the position of the error within the attribute
@@ -144,6 +150,9 @@ export function validate(xmlData, options) {
for (i++; i < xmlData.length; i++) {
if (xmlData[i] === '<') {
if (xmlData[i + 1] === '!') {
+ if (reachedRoot === true && !xmlData.startsWith('