Skip to content

Commit 1d5213e

Browse files
committed
GROOVY-12103 part2: XML markup builders don't reject PI/comment terminators in all output paths
1 parent 55a4128 commit 1d5213e

4 files changed

Lines changed: 96 additions & 0 deletions

File tree

subprojects/groovy-xml/src/main/groovy/groovy/xml/StreamingDOMBuilder.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class StreamingDOMBuilder extends AbstractStreamingBuilder {
3333
def defaultNamespaceStack = ['']
3434
/** Closure backing {@code mkp.comment} for DOM comment node creation. */
3535
def commentClosure = {doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, dom ->
36+
checkCommentText(body)
3637
def comment = dom.document.createComment(body)
3738
if (comment != null) {
3839
dom.element.appendChild(comment)
@@ -41,10 +42,12 @@ class StreamingDOMBuilder extends AbstractStreamingBuilder {
4142
/** Closure backing {@code mkp.pi} for DOM processing-instruction node creation. */
4243
def piClosure = {doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, dom ->
4344
attrs.each {target, instruction ->
45+
checkProcessingInstruction(target)
4446
def pi = null
4547
if (instruction instanceof Map) {
4648
pi = dom.document.createProcessingInstruction(target, toMapStringClosure(instruction))
4749
} else {
50+
checkProcessingInstruction(instruction)
4851
pi = dom.document.createProcessingInstruction(target, instruction)
4952
}
5053
if (pi != null) {

subprojects/groovy-xml/src/main/groovy/groovy/xml/StreamingMarkupBuilder.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class StreamingMarkupBuilder extends AbstractStreamingBuilder {
7171
* Invoked by calling <code>mkp.comment</code>
7272
*/
7373
def commentClosure = {doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, out ->
74+
checkCommentText(body)
7475
out.unescaped() << '<!--'
7576
out.escaped() << body
7677
out.unescaped() << '-->'
@@ -81,6 +82,7 @@ class StreamingMarkupBuilder extends AbstractStreamingBuilder {
8182
*/
8283
def piClosure = {doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, out ->
8384
attrs.each {target, instruction ->
85+
checkProcessingInstruction(target)
8486
out.unescaped() << '<?'
8587
if (instruction instanceof Map) {
8688
out.unescaped() << target
@@ -89,6 +91,7 @@ class StreamingMarkupBuilder extends AbstractStreamingBuilder {
8991
valueStr.contains('\'') || (useDoubleQuotes && !valueStr.contains('"'))
9092
}
9193
} else {
94+
checkProcessingInstruction(instruction)
9295
out.unescaped() << "$target $instruction"
9396
}
9497
out.unescaped() << '?>'

subprojects/groovy-xml/src/main/groovy/groovy/xml/streamingmarkupsupport/AbstractStreamingBuilder.groovy

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,32 @@ class AbstractStreamingBuilder {
8989
buf.toString()
9090
}
9191

92+
/**
93+
* Rejects a processing-instruction target or data fragment that would prematurely
94+
* close the PI. The {@code ?>} sequence ends a PI and cannot be entity-escaped, so
95+
* content holding it would inject sibling markup; such content is rejected instead.
96+
*/
97+
def checkProcessingInstruction = { part ->
98+
if (part?.toString()?.contains('?>')) {
99+
throw new GroovyRuntimeException("Processing instruction target/data must not contain '?>': ${part}")
100+
}
101+
part
102+
}
103+
104+
/**
105+
* Rejects comment text that would prematurely close the comment. Comment content
106+
* cannot contain {@code --} or end with {@code -} (which would form {@code --} against
107+
* the closing delimiter), and no escaping is legal inside a comment, so such text is
108+
* rejected instead.
109+
*/
110+
def checkCommentText = { text ->
111+
def value = text?.toString()
112+
if (value != null && (value.contains('--') || value.endsWith('-'))) {
113+
throw new GroovyRuntimeException("XML comment text must not contain '--' or end with '-': ${text}")
114+
}
115+
text
116+
}
117+
92118
/** Registry of built-in {@code mkp} helper tags shared by concrete streaming builders. */
93119
def specialTags = ['declareNamespace':namespaceSetupClosure,
94120
'declareAlias':aliasSetupClosure,

subprojects/groovy-xml/src/test/groovy/groovy/xml/MarkupBuilderInjectionTest.groovy

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,68 @@ final class MarkupBuilderInjectionTest {
6262
def result = smb.bind { mkp.pi('xml-stylesheet': [href: 'style.css', type: 'text/css']); root('x') }.toString()
6363
assertTrue(result.contains('<?xml-stylesheet') && result.contains('style.css') && result.contains('?>'))
6464
}
65+
66+
@Test
67+
void streamingProcessingInstructionRejectsTerminatorInTarget() {
68+
def smb = new StreamingMarkupBuilder()
69+
assertThrows(GroovyRuntimeException) {
70+
smb.bind { mkp.pi('x?><injected/>': [href: 'a']); root('x') }.toString()
71+
}
72+
}
73+
74+
@Test
75+
void streamingProcessingInstructionRejectsTerminatorInNonMapData() {
76+
def smb = new StreamingMarkupBuilder()
77+
assertThrows(GroovyRuntimeException) {
78+
smb.bind { mkp.pi('target': 'data?><injected/>'); root('x') }.toString()
79+
}
80+
}
81+
82+
@Test
83+
void streamingProcessingInstructionKeepsPlainNonMapData() {
84+
def smb = new StreamingMarkupBuilder()
85+
def result = smb.bind { mkp.pi('target': 'plain data'); root('x') }.toString()
86+
assertTrue(result.contains('<?target plain data?>'))
87+
}
88+
89+
@Test
90+
void streamingCommentRejectsCommentTerminator() {
91+
def smb = new StreamingMarkupBuilder()
92+
assertThrows(GroovyRuntimeException) {
93+
smb.bind { mkp.comment('legit --> <injected/> rest'); root('x') }.toString()
94+
}
95+
assertThrows(GroovyRuntimeException) {
96+
smb.bind { mkp.comment('a -- b'); root('x') }.toString()
97+
}
98+
assertThrows(GroovyRuntimeException) {
99+
smb.bind { mkp.comment('ends with dash-'); root('x') }.toString()
100+
}
101+
}
102+
103+
@Test
104+
void streamingCommentKeepsPlainText() {
105+
def smb = new StreamingMarkupBuilder()
106+
def result = smb.bind { mkp.comment('plain text'); root('x') }.toString()
107+
assertTrue(result.contains('<!--plain text-->'))
108+
}
109+
110+
@Test
111+
void streamingDomProcessingInstructionRejectsTerminator() {
112+
assertThrows(GroovyRuntimeException) {
113+
new StreamingDOMBuilder().bind { mkp.pi('xml-stylesheet': [href: 'a?><injected/>']); root('x') }()
114+
}
115+
assertThrows(GroovyRuntimeException) {
116+
new StreamingDOMBuilder().bind { mkp.pi('x?><injected/>': [href: 'a']); root('x') }()
117+
}
118+
assertThrows(GroovyRuntimeException) {
119+
new StreamingDOMBuilder().bind { mkp.pi('target': 'data?><injected/>'); root('x') }()
120+
}
121+
}
122+
123+
@Test
124+
void streamingDomCommentRejectsCommentTerminator() {
125+
assertThrows(GroovyRuntimeException) {
126+
new StreamingDOMBuilder().bind { mkp.comment('a -- b'); root('x') }()
127+
}
128+
}
65129
}

0 commit comments

Comments
 (0)