Skip to content

Commit 8787c39

Browse files
committed
Sanitize input for custom parsers
1 parent 586e0f7 commit 8787c39

4 files changed

Lines changed: 87 additions & 31 deletions

File tree

‎src/builder/dom.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function sanitizeInput(str: string | null | undefined,
2929
* Sanitizes input strings with user supplied replacement characters.
3030
*
3131
* @param str - input string
32+
* @param replacement - replacement character or function
3233
*/
3334
export function sanitizeInput(str: any,
3435
replacement?: string | ((char: string, offset: number, str: string) => string)): any {

‎src/readers/ObjectReader.ts‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
forEachObject, isEmpty
55
} from "@oozcitak/util"
66
import { BaseReader } from "./BaseReader"
7+
import { sanitizeInput } from "../builder/dom"
78

89
/**
910
* Parses XML nodes from objects and arrays.
@@ -21,6 +22,12 @@ export class ObjectReader extends BaseReader<ExpandObject> {
2122

2223
const options = this._builderOptions
2324

25+
// sanitizes input characters
26+
const invalidCharReplacement = options.invalidCharReplacement
27+
const s = function (str: string): string {
28+
return sanitizeInput(str, invalidCharReplacement)
29+
}
30+
2431
let lastChild: XMLBuilder | null = null
2532

2633
if (isFunction(obj)) {
@@ -43,56 +50,61 @@ export class ObjectReader extends BaseReader<ExpandObject> {
4350
throw new Error("Invalid attribute: " + val.toString() + ". " + (node as any)._debugInfo())
4451
} else /* if (isMap(val) || isObject(val)) */ {
4552
forEachObject(val, (attrKey, attrVal) => {
46-
lastChild = this.attribute(node, undefined, attrKey, attrVal as string) || lastChild
53+
lastChild = this.attribute(node, undefined,
54+
s(attrKey), s(attrVal as string)) || lastChild
4755
})
4856
}
4957
} else {
50-
lastChild = this.attribute(node, undefined, key.substr(options.convert.att.length), val) || lastChild
58+
lastChild = this.attribute(node, undefined,
59+
s(key.substr(options.convert.att.length)),
60+
s(val)) || lastChild
5161
}
5262
} else if (!options.ignoreConverters && key.indexOf(options.convert.text) === 0) {
5363
// text node
5464
if (isMap(val) || isObject(val)) {
5565
// if the key is #text expand child nodes under this node to support mixed content
5666
lastChild = this.parse(node, val)
5767
} else {
58-
lastChild = this.text(node, val) || lastChild
68+
lastChild = this.text(node, s(val)) || lastChild
5969
}
6070
} else if (!options.ignoreConverters && key.indexOf(options.convert.cdata) === 0) {
6171
// cdata node
6272
if (isArray(val) || isSet(val)) {
63-
forEachArray(val, item => lastChild = this.cdata(node, item) || lastChild, this)
73+
forEachArray(val, item => lastChild = this.cdata(node, s(item)) || lastChild, this)
6474
} else {
65-
lastChild = this.cdata(node, val) || lastChild
75+
lastChild = this.cdata(node, s(val)) || lastChild
6676
}
6777
} else if (!options.ignoreConverters && key.indexOf(options.convert.comment) === 0) {
6878
// comment node
6979
if (isArray(val) || isSet(val)) {
70-
forEachArray(val, item => lastChild = this.comment(node, item) || lastChild, this)
80+
forEachArray(val, item => lastChild = this.comment(node, s(item)) || lastChild, this)
7181
} else {
72-
lastChild = this.comment(node, val) || lastChild
82+
lastChild = this.comment(node, s(val)) || lastChild
7383
}
7484
} else if (!options.ignoreConverters && key.indexOf(options.convert.ins) === 0) {
7585
// processing instruction
7686
if (isString(val)) {
7787
const insIndex = val.indexOf(' ')
7888
const insTarget = (insIndex === -1 ? val : val.substr(0, insIndex))
7989
const insValue = (insIndex === -1 ? '' : val.substr(insIndex + 1))
80-
lastChild = this.instruction(node, insTarget, insValue) || lastChild
90+
lastChild = this.instruction(node, s(insTarget), s(insValue)) || lastChild
8191
} else if (isArray(val) || isSet(val)) {
8292
forEachArray(val, item => {
8393
const insIndex = item.indexOf(' ')
8494
const insTarget = (insIndex === -1 ? item : item.substr(0, insIndex))
8595
const insValue = (insIndex === -1 ? '' : item.substr(insIndex + 1))
86-
lastChild = this.instruction(node, insTarget, insValue) || lastChild
96+
lastChild = this.instruction(node, s(insTarget), s(insValue)) || lastChild
8797
}, this)
8898
} else /* if (isMap(target) || isObject(target)) */ {
89-
forEachObject(val, (insTarget, insValue) => lastChild = this.instruction(node, insTarget, insValue as string) || lastChild, this)
99+
forEachObject(val, (insTarget, insValue) => lastChild = this.instruction(node,
100+
s(insTarget),
101+
s(insValue as string)) || lastChild, this)
90102
}
91103
} else if ((isArray(val) || isSet(val)) && isEmpty(val)) {
92104
// skip empty arrays
93105
} else if ((isMap(val) || isObject(val)) && isEmpty(val)) {
94106
// empty objects produce one node
95-
lastChild = this.element(node, undefined, key) || lastChild
107+
lastChild = this.element(node, undefined, s(key)) || lastChild
96108
} else if (!options.keepNullNodes && (val == null)) {
97109
// skip null and undefined nodes
98110
} else if (isArray(val) || isSet(val)) {
@@ -115,11 +127,11 @@ export class ObjectReader extends BaseReader<ExpandObject> {
115127
const parent = this.element(node, undefined, key)
116128
if (parent) {
117129
lastChild = parent
118-
this.text(parent, val)
130+
this.text(parent, s(val))
119131
}
120132
} else {
121133
// leaf element node
122-
lastChild = this.element(node, undefined, key) || lastChild
134+
lastChild = this.element(node, undefined, s(key)) || lastChild
123135
}
124136
}, this)
125137
}

‎src/readers/XMLReader.ts‎

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,61 +23,68 @@ export class XMLReader extends BaseReader<string> {
2323
_parse(node: XMLBuilder, str: string): XMLBuilder {
2424
const lexer = new XMLStringLexer(str, { skipWhitespaceOnlyText: true })
2525

26+
// sanitizes input characters
27+
const invalidCharReplacement = this._builderOptions.invalidCharReplacement
28+
const s = function(str: string): string {
29+
return sanitizeInput(str, invalidCharReplacement)
30+
}
31+
2632
let context = node
2733
let token = lexer.nextToken()
2834
while (token.type !== TokenType.EOF) {
2935
switch (token.type) {
3036
case TokenType.Declaration:
3137
const declaration = <DeclarationToken>token
32-
if (declaration.version !== "1.0") {
33-
throw new Error("Invalid xml version: " + declaration.version)
38+
const version = s(declaration.version)
39+
if (version !== "1.0") {
40+
throw new Error("Invalid xml version: " + version)
3441
}
3542
const builderOptions: Partial<XMLBuilderOptions> = {
36-
version: declaration.version
43+
version: version
3744
}
3845
if (declaration.encoding) {
39-
builderOptions.encoding = declaration.encoding
46+
builderOptions.encoding = s(declaration.encoding)
4047
}
4148
if (declaration.standalone) {
42-
builderOptions.standalone = (declaration.standalone === "yes")
49+
builderOptions.standalone = (s(declaration.standalone) === "yes")
4350
}
4451
context.set(builderOptions)
4552
break
4653
case TokenType.DocType:
4754
const doctype = <DocTypeToken>token
48-
context = this.docType(context, doctype.name, doctype.pubId, doctype.sysId) || context
55+
context = this.docType(context, s(doctype.name), s(doctype.pubId), s(doctype.sysId)) || context
4956
break
5057
case TokenType.CDATA:
5158
const cdata = <CDATAToken>token
52-
context = this.cdata(context, cdata.data) || context
59+
context = this.cdata(context, s(cdata.data)) || context
5360
break
5461
case TokenType.Comment:
5562
const comment = <CommentToken>token
56-
context = this.comment(context, comment.data) || context
63+
context = this.comment(context, s(comment.data)) || context
5764
break
5865
case TokenType.PI:
5966
const pi = <PIToken>token
60-
context = this.instruction(context, pi.target, pi.data) || context
67+
context = this.instruction(context, s(pi.target), s(pi.data)) || context
6168
break
6269
case TokenType.Text:
6370
const text = <TextToken>token
64-
context = this.text(context, text.data) || context
71+
context = this.text(context, s(text.data)) || context
6572
break
6673
case TokenType.Element:
6774
const element = <ElementToken>token
75+
const elementName = s(element.name)
6876

6977
// inherit namespace from parent
70-
const [prefix] = namespace_extractQName(
71-
sanitizeInput(element.name, this._builderOptions.invalidCharReplacement))
78+
const [prefix] = namespace_extractQName(elementName)
7279
let namespace = context.node.lookupNamespaceURI(prefix)
7380

7481
// override namespace if there is a namespace declaration
7582
// attribute
7683
// also lookup namespace declaration attributes
7784
const nsDeclarations: { [key: string]: string } = {}
7885
for (let [attName, attValue] of element.attributes) {
79-
attName = sanitizeInput(attName, this._builderOptions.invalidCharReplacement)
80-
attValue = sanitizeInput(attValue, this._builderOptions.invalidCharReplacement)
86+
attName = s(attName)
87+
attValue = s(attValue)
8188
if (attName === "xmlns") {
8289
namespace = attValue
8390
} else {
@@ -93,14 +100,14 @@ export class XMLReader extends BaseReader<string> {
93100

94101
// create the DOM element node
95102
const elementNode = (namespace !== null ?
96-
this.element(context, namespace, element.name) :
97-
this.element(context, undefined, element.name))
103+
this.element(context, namespace, elementName) :
104+
this.element(context, undefined, elementName))
98105
if (elementNode === undefined) break
99106

100107
// assign attributes
101108
for (let [attName, attValue] of element.attributes) {
102-
attName = sanitizeInput(attName, this._builderOptions.invalidCharReplacement)
103-
attValue = sanitizeInput(attValue, this._builderOptions.invalidCharReplacement)
109+
attName = s(attName)
110+
attValue = s(attValue)
104111
const [attPrefix, attLocalName] = namespace_extractQName(attName)
105112
let attNamespace: string | null = null
106113
if (attPrefix === "xmlns" || (attPrefix === null && attLocalName === "xmlns")) {

‎test/readers/XMLReader.custom.test.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import $$ from '../TestHelpers'
2+
import { sanitizeInput } from '../../src/builder/dom'
23

34
describe('custom XMLReader', () => {
45

@@ -55,4 +56,39 @@ describe('custom XMLReader', () => {
5556
`)
5657
})
5758

59+
test('skip DocType', () => {
60+
const xml = $$.t`
61+
<?xml version="1.0"?>
62+
<!DOCTYPE root PUBLIC "pub" "sys">
63+
<root xmlns="ns"/>`
64+
65+
const doc = $$.create({ parser: { docType: () => undefined } }, xml).doc()
66+
expect(doc.end()).toBe('<?xml version="1.0"?><root xmlns="ns"/>')
67+
})
68+
69+
test("invalidCharReplacement should apply before parser functions", () => {
70+
const xml = `
71+
<root>
72+
<node1\x00/>
73+
<node2/>
74+
</root>
75+
`
76+
77+
const obj = $$.convert({
78+
invalidCharReplacement: '',
79+
parser: {
80+
element: (parent, ns, name: string) => {
81+
expect(sanitizeInput(name, '')).toBe(name)
82+
return parent.ele(name)
83+
}
84+
}
85+
}, xml, { format: 'object' })
86+
expect(obj).toEqual({
87+
root: {
88+
node1: {},
89+
node2: {}
90+
}
91+
})
92+
})
93+
5894
})

0 commit comments

Comments
 (0)