Skip to content

Commit feee873

Browse files
committed
adding entity check for xml parser + xml sanitizer
1 parent 7a5d5d9 commit feee873

File tree

7 files changed

+84
-12
lines changed

7 files changed

+84
-12
lines changed

player/service/package-lock.json

+18-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

player/service/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"mysql": "^2.18.1",
4444
"node-stream-zip": "^1.13.6",
4545
"uuid": "^9.0.1",
46-
"wait-port": "^0.2.9"
46+
"wait-port": "^0.2.9",
47+
"xml-sanitizer": "^2.0.1"
4748
},
4849
"devDependencies": {
4950
"chai": "^4",

player/service/plugins/routes/lib/helpers.js

+25
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
const Boom = require("@hapi/boom");
1919
const Requirements = require("@cmi5/requirements");
2020
const axios = require("axios").default;
21+
const xmlSanitizer = require("xml-sanitizer");
2122

2223
let user = process.env.LRS_USERNAME;
2324
let pass = process.env.LRS_PASSWORD;
@@ -133,5 +134,29 @@ module.exports = {
133134
];
134135

135136
return concurrencyPaths.includes(resourcePath);
137+
},
138+
139+
/**
140+
* Checks the given XML content, returning false if the content
141+
* appears potentially malicious.
142+
* @param {String|Buffer} xmlContent
143+
*/
144+
isPotentiallyMaliciousXML: async(xmlContent) => {
145+
146+
if (xmlContent.includes("<!ENTITY")) {
147+
console.error(">> Invalid XML -- Attempts an Entity Tag: ", "\n--------------\n", xmlContent);
148+
return true;
149+
}
150+
151+
return false;
152+
},
153+
154+
/**
155+
* Checks the given XML content, returning false if the content
156+
* appears potentially malicious.
157+
* @param {String|Buffer} xmlContent
158+
*/
159+
sanitizeXML: async(xmlContent) => {
160+
return xmlSanitizer(xmlContent);
136161
}
137162
};

player/service/plugins/routes/v1/courses.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const uuidv4 = require("uuid").v4;
2828
const Helpers = require("../lib/helpers");
2929
const Registration = require("../lib/registration");
3030
const Session = require("../lib/session");
31+
const helpers = require("../lib/helpers");
3132
const readFile = util.promisify(fs.readFile);
3233
const copyFile = util.promisify(fs.copyFile);
3334
const mkdir = util.promisify(fs.mkdir);
@@ -349,7 +350,7 @@ module.exports = {
349350
//
350351
isZip = contentType === "application/zip" || contentType === "application/x-zip-compressed";
351352

352-
let courseStructureData,
353+
let courseStructureDataRaw,
353354
zip;
354355

355356
if (!isZip && contentType !== "text/xml") {
@@ -365,7 +366,7 @@ module.exports = {
365366
}
366367

367368
try {
368-
courseStructureData = await zip.entryData("cmi5.xml");
369+
courseStructureDataRaw = await zip.entryData("cmi5.xml");
369370
}
370371
catch (ex) {
371372
if (ex.message === "Bad archive") {
@@ -377,13 +378,18 @@ module.exports = {
377378
}
378379
else {
379380
try {
380-
courseStructureData = await readFile(req.payload.path);
381+
courseStructureDataRaw = await readFile(req.payload.path);
381382
}
382383
catch (ex) {
383384
throw Boom.internal(`Failed to read structure file: ${ex}`);
384385
}
385386
}
386387

388+
let courseStructureData = helpers.sanitizeXML(courseStructureDataRaw);
389+
if (courseStructureData != undefined && helpers.isPotentiallyMaliciousXML(courseStructureData)) {
390+
throw Boom.internal(`Invalid XML data provided: ${ex}`);
391+
}
392+
387393
let courseStructureDocument;
388394

389395
try {

player/service/tests/files/entity.xml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="ISO-8859-1"?>
2+
<!DOCTYPE foo [
3+
<!ENTITY xxe SYSTEM "file:///some/bad/place" >]>
4+
<username>&xxe;</username>
5+
</xml>

player/service/tests/lrs.spec.js

-7
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,12 @@ describe("Basic LRS Communications", async () => {
3434
};
3535

3636
let path = "/statements?statementId=" + statement.id;
37-
let pathWithoutID = "/statements";
3837

3938
console.log("Sending statement ...");
4039

4140
let res = await helpers.sendDocumentToLRS(path, "PUT", statement);
42-
let ids = res.responseBody;
4341

44-
delete res.res;
45-
46-
console.log("Received response:", res);
47-
4842
chai.expect(res.status).to.be.lessThan(400);
4943
chai.expect(res.status).to.be.greaterThanOrEqual(200);
50-
chai.expect(ids).to.have.length(1);
5144
});
5245
});

player/service/tests/xml.spec.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const path = require("path");
2+
require("dotenv").config({
3+
path: path.join(__dirname, "../../.env")
4+
});
5+
6+
const fs = require("fs");
7+
8+
const helpers = require("../plugins/routes/lib/helpers");
9+
const chai = require("chai");
10+
11+
describe("Libxmljs Usage", async () => {
12+
13+
/**
14+
* https://www.stackhawk.com/blog/nodejs-xml-external-entities-xxe-guide-examples-and-prevention/
15+
*/
16+
const PATH_XML_DOC_ENTITY_USAGE = path.join(__dirname, "files/entity.xml");
17+
18+
it ("Rejects anything using an ENTITY tag", async() => {
19+
20+
let entityBody = fs.readFileSync(PATH_XML_DOC_ENTITY_USAGE);
21+
let suspicious = await helpers.isPotentiallyMaliciousXML(entityBody);
22+
23+
chai.expect(suspicious).to.be.equal(true, "The provided XML should have thrown a validity issue for its use of an <!ENTITY tag");
24+
});
25+
});

0 commit comments

Comments
 (0)