Skip to content

Commit d6baf35

Browse files
committed
Updated security concerns
1 parent c3f5ec3 commit d6baf35

3 files changed

Lines changed: 60 additions & 34 deletions

File tree

ts/SAXParser.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -764,12 +764,15 @@ export class SAXParser {
764764
return text;
765765
}
766766

767-
return text
768-
.replace(/&lt;/g, '<')
769-
.replace(/&gt;/g, '>')
770-
.replace(/&amp;/g, '&')
771-
.replace(/&quot;/g, '"')
772-
.replace(/&apos;/g, "'");
767+
const predefinedEntities: Record<string, string> = {
768+
lt: '<',
769+
gt: '>',
770+
amp: '&',
771+
quot: '"',
772+
apos: "'",
773+
};
774+
775+
return text.replace(/&(lt|gt|amp|quot|apos);/g, (_match, entity: string) => predefinedEntities[entity]);
773776
}
774777

775778
private isXmlSpacePreserve(): boolean {

ts/grammar/GrammarHandler.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
*******************************************************************************/
1212

1313
import { existsSync } from 'fs';
14-
import { dirname, isAbsolute, resolve } from 'path';
14+
import { dirname, isAbsolute, relative, resolve } from 'path';
1515
import { fileURLToPath } from 'url';
1616
import { Catalog } from '../Catalog';
1717
import { DTDGrammar } from '../dtd/DTDGrammar';
1818
import { DTDParser } from '../dtd/DTDParser';
1919
import { EntityDecl } from '../dtd/EntityDecl';
20-
import { XMLUtils } from '../XMLUtils';
2120
import { XMLSchemaParser } from '../schema/XMLSchemaParser';
21+
import { XMLUtils } from '../XMLUtils';
2222
import { CompositeGrammar } from './CompositeGrammar';
2323
import { DTDComposite } from './DTDComposite';
2424
import { Grammar } from './Grammar';
@@ -487,7 +487,13 @@ export class GrammarHandler {
487487
// This is a relative path - resolve it relative to the current document
488488
if (this.currentFile) {
489489
const documentDir: string = dirname(this.currentFile);
490-
resolvedLocation = resolve(documentDir, location);
490+
const candidate: string = resolve(documentDir, location);
491+
const relativeFromDoc: string = relative(documentDir, candidate);
492+
if (relativeFromDoc.startsWith('..')) {
493+
this.trace(`Rejected relative schema reference '${location}' for namespace '${namespace}' because it resolves outside the source directory`);
494+
return;
495+
}
496+
resolvedLocation = candidate;
491497
this.trace(`Resolved relative schema reference '${location}' to '${resolvedLocation}' for namespace '${namespace}'`);
492498
} else {
493499
this.trace(`Cannot resolve relative schema reference '${location}' for namespace '${namespace}' because current file is unknown`);

ts/schema/BuiltinTypes.ts

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
* Maxprograms - initial API and implementation
1111
*******************************************************************************/
1212

13+
import { ValidationResult } from "../grammar/Grammar";
1314
import { XMLUtils } from "../XMLUtils";
1415
import { SimpleType } from "./SimpleType";
15-
import { ValidationResult } from "../grammar/Grammar";
1616

1717
export class BuiltinTypes {
1818
private static types: Map<string, SimpleType> = new Map();
@@ -28,7 +28,7 @@ export class BuiltinTypes {
2828
// Base types
2929
this.createType("anyType", xsNamespace, () => ValidationResult.success());
3030
this.createType("anySimpleType", xsNamespace, () => ValidationResult.success());
31-
31+
3232
// String types
3333
this.createType("string", xsNamespace, this.validateString);
3434
this.createType("normalizedString", xsNamespace, this.validateNormalizedString);
@@ -40,7 +40,7 @@ export class BuiltinTypes {
4040
this.createType("IDREF", xsNamespace, this.validateIDREF);
4141
this.createType("ENTITY", xsNamespace, BuiltinTypes.validateNCName);
4242
this.createType("NMTOKEN", xsNamespace, this.validateNMTOKEN);
43-
43+
4444
// Numeric types
4545
this.createType("decimal", xsNamespace, this.validateDecimal);
4646
this.createType("integer", xsNamespace, BuiltinTypes.validateInteger);
@@ -58,7 +58,7 @@ export class BuiltinTypes {
5858
this.createType("unsignedByte", xsNamespace, this.validateUnsignedByte);
5959
this.createType("float", xsNamespace, this.validateFloat);
6060
this.createType("double", xsNamespace, this.validateDouble);
61-
61+
6262
// Date/time types
6363
this.createType("duration", xsNamespace, this.validateDuration);
6464
this.createType("dateTime", xsNamespace, this.validateDateTime);
@@ -69,7 +69,7 @@ export class BuiltinTypes {
6969
this.createType("gMonthDay", xsNamespace, this.validateGMonthDay);
7070
this.createType("gDay", xsNamespace, this.validateGDay);
7171
this.createType("gMonth", xsNamespace, this.validateGMonth);
72-
72+
7373
// Other types
7474
this.createType("boolean", xsNamespace, this.validateBoolean);
7575
this.createType("base64Binary", xsNamespace, this.validateBase64Binary);
@@ -121,19 +121,36 @@ export class BuiltinTypes {
121121
if (!normalized.isValid) {
122122
return normalized;
123123
}
124-
124+
125125
if (value !== value.trim() || /\s{2,}/.test(value)) {
126126
return ValidationResult.error("token cannot have leading/trailing whitespace or consecutive spaces");
127127
}
128128
return ValidationResult.success();
129129
}
130130

131131
private static validateLanguage(value: string): ValidationResult {
132-
// RFC 3066 language codes
133-
const pattern = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/;
134-
if (!pattern.test(value)) {
132+
// RFC 3066 language codes - use iterative checks to avoid regex backtracking issues
133+
if (!value) {
135134
return ValidationResult.error("Invalid language code format");
136135
}
136+
137+
const segments: string[] = value.split('-');
138+
if (segments.length === 0) {
139+
return ValidationResult.error("Invalid language code format");
140+
}
141+
142+
const primaryTag: string = segments[0];
143+
if (!/^[a-zA-Z]{1,8}$/.test(primaryTag)) {
144+
return ValidationResult.error("Invalid language code format");
145+
}
146+
147+
for (let i = 1; i < segments.length; i++) {
148+
const subTag: string = segments[i];
149+
if (!/^[a-zA-Z0-9]{1,8}$/.test(subTag)) {
150+
return ValidationResult.error("Invalid language code format");
151+
}
152+
}
153+
137154
return ValidationResult.success();
138155
}
139156

@@ -152,7 +169,7 @@ export class BuiltinTypes {
152169
if (!nameResult.isValid) {
153170
return nameResult;
154171
}
155-
172+
156173
if (value.includes(':')) {
157174
return ValidationResult.error("NCName cannot contain colon");
158175
}
@@ -198,7 +215,7 @@ export class BuiltinTypes {
198215
if (!intResult.isValid) {
199216
return intResult;
200217
}
201-
218+
202219
const num = parseInt(value, 10);
203220
if (num > 0) {
204221
return ValidationResult.error("nonPositiveInteger must be <= 0");
@@ -211,7 +228,7 @@ export class BuiltinTypes {
211228
if (!intResult.isValid) {
212229
return intResult;
213230
}
214-
231+
215232
const num = parseInt(value, 10);
216233
if (num >= 0) {
217234
return ValidationResult.error("negativeInteger must be < 0");
@@ -224,7 +241,7 @@ export class BuiltinTypes {
224241
if (!intResult.isValid) {
225242
return intResult;
226243
}
227-
244+
228245
const num = parseInt(value, 10);
229246
if (num < -9223372036854775808 || num > 9223372036854775807) {
230247
return ValidationResult.error("long value out of range");
@@ -237,7 +254,7 @@ export class BuiltinTypes {
237254
if (!intResult.isValid) {
238255
return intResult;
239256
}
240-
257+
241258
const num = parseInt(value, 10);
242259
if (num < -2147483648 || num > 2147483647) {
243260
return ValidationResult.error("int value out of range");
@@ -250,7 +267,7 @@ export class BuiltinTypes {
250267
if (!intResult.isValid) {
251268
return intResult;
252269
}
253-
270+
254271
const num = parseInt(value, 10);
255272
if (num < -32768 || num > 32767) {
256273
return ValidationResult.error("short value out of range");
@@ -263,7 +280,7 @@ export class BuiltinTypes {
263280
if (!intResult.isValid) {
264281
return intResult;
265282
}
266-
283+
267284
const num = parseInt(value, 10);
268285
if (num < -128 || num > 127) {
269286
return ValidationResult.error("byte value out of range");
@@ -276,7 +293,7 @@ export class BuiltinTypes {
276293
if (!intResult.isValid) {
277294
return intResult;
278295
}
279-
296+
280297
const num = parseInt(value, 10);
281298
if (num < 0) {
282299
return ValidationResult.error("nonNegativeInteger must be >= 0");
@@ -289,7 +306,7 @@ export class BuiltinTypes {
289306
if (!intResult.isValid) {
290307
return intResult;
291308
}
292-
309+
293310
const num = parseInt(value, 10);
294311
if (num <= 0) {
295312
return ValidationResult.error("positiveInteger must be > 0");
@@ -302,7 +319,7 @@ export class BuiltinTypes {
302319
if (!nonNegResult.isValid) {
303320
return nonNegResult;
304321
}
305-
322+
306323
const num = parseInt(value, 10);
307324
if (num > 18446744073709551615) {
308325
return ValidationResult.error("unsignedLong value out of range");
@@ -315,7 +332,7 @@ export class BuiltinTypes {
315332
if (!nonNegResult.isValid) {
316333
return nonNegResult;
317334
}
318-
335+
319336
const num = parseInt(value, 10);
320337
if (num > 4294967295) {
321338
return ValidationResult.error("unsignedInt value out of range");
@@ -328,7 +345,7 @@ export class BuiltinTypes {
328345
if (!nonNegResult.isValid) {
329346
return nonNegResult;
330347
}
331-
348+
332349
const num = parseInt(value, 10);
333350
if (num > 65535) {
334351
return ValidationResult.error("unsignedShort value out of range");
@@ -341,7 +358,7 @@ export class BuiltinTypes {
341358
if (!nonNegResult.isValid) {
342359
return nonNegResult;
343360
}
344-
361+
345362
const num = parseInt(value, 10);
346363
if (num > 255) {
347364
return ValidationResult.error("unsignedByte value out of range");
@@ -353,7 +370,7 @@ export class BuiltinTypes {
353370
if (value === "INF" || value === "-INF" || value === "NaN") {
354371
return ValidationResult.success();
355372
}
356-
373+
357374
const num = parseFloat(value);
358375
if (isNaN(num)) {
359376
return ValidationResult.error("Invalid float format");
@@ -492,14 +509,14 @@ export class BuiltinTypes {
492509
if (parts.length > 2) {
493510
return ValidationResult.error("QName can have at most one colon");
494511
}
495-
512+
496513
for (const part of parts) {
497514
const ncNameResult = BuiltinTypes.validateNCName(part);
498515
if (!ncNameResult.isValid) {
499516
return ValidationResult.error("Invalid QName: " + ncNameResult.errors[0].message);
500517
}
501518
}
502-
519+
503520
return ValidationResult.success();
504521
}
505522
}

0 commit comments

Comments
 (0)