Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
23bca3d
Adding the commit from 1488 called "fix: resolved PR feedback on sani…
Landon-Wivell Jul 7, 2025
bacc35d
feat(security): centralize DOMPurify sanitizer and patch XSS in succe…
Landon-Wivell May 8, 2025
979c3de
Added the commit from 1488 called "feat(security): centralize DOMPuri…
Zachary-Squires Sep 30, 2025
7cfcb79
fix: resolved PR feedback on sanitizer imports and tests
Landon-Wivell Jul 7, 2025
e642e0e
Ran npm install, should bring jsdom and dompurify
Zachary-Squires Sep 30, 2025
7cb2438
Restore executable permissions on scripts and binaries
Zachary-Squires Oct 7, 2025
2ceee19
Merge pull request #1 from Andrew-Bonner/team_b
Zachary-Squires Oct 7, 2025
c24d3c4
Normalize all line endings to LF
Zachary-Squires Oct 7, 2025
f38a83e
Merge pull request #2 from Andrew-Bonner/team_b
Zachary-Squires Oct 7, 2025
ad31fd0
Change to not allow default postgres or secret token in OED production.
Zachary-Squires Nov 4, 2025
2ba98fc
Merge branch 'OpenEnergyDashboard:development' into team_b
Zachary-Squires Nov 4, 2025
f5d6ce0
Merge branch 'team_b' of https://github.com/Andrew-Bonner/OED into te…
Zachary-Squires Nov 4, 2025
489cc17
Merge pull request #3 from Andrew-Bonner/team_b
Zachary-Squires Nov 4, 2025
3c3b6cc
Fix PR suggestions
Zach-O-Bates Nov 5, 2025
6eb6d29
Revert "Merge pull request #3 from Andrew-Bonner/team_b"
Zachary-Squires Nov 5, 2025
7611ae5
Merge branch 'team_b' into development
Zach-O-Bates Nov 5, 2025
040f138
Simplify test to better align with mentor suggestions
Zach-O-Bates Nov 11, 2025
da2dd00
Update .gitattributes
Zachary-Squires Nov 12, 2025
5a40623
Update docker-compose.yml
Zachary-Squires Nov 12, 2025
9acd4b6
Update jsdom package
Zach-O-Bates Nov 12, 2025
c03723a
Addressing Suggestions for PR 1544
Zachary-Squires Nov 20, 2025
533c588
Mark cron scripts as executable
Zachary-Squires Nov 20, 2025
df29aaa
Mark docker-compose.yml as executable
Zachary-Squires Nov 20, 2025
80382e2
Update docker-compose.yml
Zachary-Squires Nov 20, 2025
fea6028
try to resolve some blank line change issues
huss Nov 24, 2025
2ba9168
Merge remote-tracking branch 'origin/development' into pr/Zachary-Squ…
huss Nov 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
533 changes: 530 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@
"csv": "~5.3.2",
"csv-stringify": "~5.6.5",
"d3": "~7.8.5",
"dompurify": "~3.3.0",
"dotenv": "~16.4.5",
"escape-html": "~1.0.3",
"express": "~4.19.2",
"express-rate-limit": "~7.2.0",
"history": "~5.3.0",
"ini": "~4.1.3",
"jsdom": "~27.2.0",
"jsonschema": "~1.4.1",
"jsonwebtoken": "~9.0.0",
"lodash": "~4.17.21",
Expand Down
31 changes: 15 additions & 16 deletions src/server/routes/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,31 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// Functions to return a code and comment from an Express request.
// Functions to return a code and comment from an Express request.

const DOMPurify = require('../services/utils/sanitizer');

/**
* Inform the client of a success (200 OK).
*
* @param res The Express response object
* @param comment Any additional data to be returned to the client as a string
* Inform the client of a success (200 OK) with sanitized content.
*
* @param {express.Response} res The Express response object.
* @param {string} comment Any additional data to be returned to the client as a string.
*/
function success(res, comment = '') {
res.status(200) // 200 OK
.send(comment);
function success(res, comment = '') {
const safeComment = DOMPurify.sanitize(comment);
res.status(200).send(safeComment);
}

/**
* Inform the client of a failure with provided code or 500.
*
* @param res The Express response object
* @param code The code number to send back for request
* @param comment Any additional data to be returned to the client as a string
* Inform the client of a failure with provided code or 500, using sanitized content.
*
* @param {express.Response} res The Express response object.
* @param {number} code The code number to send back for the request.
* @param {string} comment Any additional data to be returned to the client as a string.
*/
function failure(res, code = 500, comment = '') {
res.status(code)
.send(comment);

const safeComment = DOMPurify.sanitize(comment);
res.status(code).send(safeComment);
}

module.exports = { success, failure };
28 changes: 13 additions & 15 deletions src/server/services/csvPipeline/success.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,33 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const express = require('express') /* needed to resolve types in JSDoc comments */
const express = require('express');
const DOMPurify = require('../utils/sanitizer');

/**
* Inform the client of a success (200 OK).
* Inform the client of a success (200 OK) with sanitized HTML content.
*
* @param {express.Request} req The Express request object
* @param {express.Response} res The Express response object
* @param {express.Request} req The Express request object.
* @param {express.Response} res The Express response object.
* @param {string} comment Any additional data to be returned to the client.
*
*/
function success(req, res, comment = '') {
res.status(200) // 200 OK
.send(`<h1>SUCCESS</h1>${comment}`);
const safeComment = DOMPurify.sanitize(comment);
res.status(200).send(`<h1>SUCCESS</h1>${safeComment}`);
}

/**
* Inform the client of a failure (400 OK).
* Inform the client of a failure (400 OK) with sanitized HTML content.
*
* @param {express.Request} req The Express request object
* @param {express.Response} res The Express response object
* @param {express.Request} req The Express request object.
* @param {express.Response} res The Express response object.
* @param {string} comment Any additional data to be returned to the client.
*
*/
function failure(req, res, comment = '') {
const safeComment = DOMPurify.sanitize(comment);
// 400 is client error. There is a small chance the insert into the DB failed
// but overlooking that.
res.status(400)
.send(`<h1>FAILURE</h1>${comment}`);

res.status(400).send(`<h1>FAILURE</h1>${safeComment}`);
}

module.exports = { success, failure };
module.exports = { success, failure };
12 changes: 12 additions & 0 deletions src/server/services/utils/sanitizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const { JSDOM } = require('jsdom');
const createDOMPurify = require('dompurify');

// Create jsdom window application and initialize
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

module.exports = DOMPurify;
28 changes: 28 additions & 0 deletions src/server/test/crossSite/crossSite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* This file tests the functionality of the DOMPurify library. It tests for XSS
vulnerabilities in HTML of user uploaded data.*/

/* Run in OED Docker web container terminal/shell:
npm run testsome src/server/test/crossSite/crossSite.js */
const { chai, mocha, expect, app, testUser } = require('../common');

mocha.describe('Cross site', () => {
mocha.it('Test for sanitization of HTML', async () => {
const filePath = 'src/server/test/crossSite/readings.csv';

const res = await chai.request(app).post('/api/csv/readings')
.field('email', testUser.username)
.field('password', testUser.password)
/* This next line should produce:
res.text: <h1>FAILURE</h1>CSVPipelineError:
User Error: Meter with name '<img src="x">' not found. */
.field('meterName','<img src=x onerror="alert(document.domain)">')
.field('gzip', "no")
.attach('csvfile', 'src/server/test/crossSite/something.csv');
expect(res).to.have.status(400);
expect(res.text).to.include('<img src="x">');
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think testing for .not having "alert" (and maybe other strings?) would strengthen this test.

})
1 change: 1 addition & 0 deletions src/server/test/crossSite/something.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1,2,3
201 changes: 2 additions & 199 deletions src/server/test/web/readingsCompareGroupFlow.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,210 +213,13 @@ mocha.describe('readings API', () => {
expectCompareToEqualExpected(res, expected, GROUP_ID);
});

mocha.it('CG17: 1 full day shift for 15 minute reading intervals and flow units & kW as kW ', async () => {

const unitDatakW = [
{
// u4
name: 'kW',
identifier: '',
unitRepresent: Unit.unitRepresentType.FLOW,
secInRate: 3600,
typeOfUnit: Unit.unitType.UNIT,
suffix: '',
displayable: Unit.displayableType.ALL,
preferredDisplay: true,
note: 'kilowatts'
},
{
// u5
name: 'Electric',
identifier: '',
unitRepresent: Unit.unitRepresentType.FLOW,
secInRate: 3600,
typeOfUnit: Unit.unitType.METER,
suffix: '',
displayable: Unit.displayableType.NONE,
preferredDisplay: false,
note: 'special unit'
},
];
// conversion data
const conversionDatakW = [
{
// c4
sourceName: 'Electric',
destinationName: 'kW',
bidirectional: false,
slope: 1,
intercept: 0,
note: 'Electric → kW'
}
];
// meter groups
const meterDatakWGroups = [
{
name: 'meterDatakW',
unit: 'Electric',
defaultGraphicUnit: 'kW',
displayable: true,
gps: undefined,
note: 'special meter',
file: 'test/web/readingsData/readings_ri_15_days_75.csv',
deleteFile: false,
readingFrequency: '15 minutes',
id: METER_ID
},
{
name: 'meterDatakWOther',
unit: 'Electric',
defaultGraphicUnit: 'kW',
displayable: true,
gps: undefined,
note: 'special meter',
file: 'test/web/readingsData/readings_ri_20_days_75.csv',
deleteFile: false,
readingFrequency: '20 minutes',
id: (METER_ID + 1)
}
];
// group data
const groupDatakW = [
{
id: GROUP_ID,
name: 'meterDatakW + meterDatakWOther',
displayable: true,
note: 'special group',
defaultGraphicUnit: 'kW',
childMeters: ['meterDatakW', 'meterDatakWOther'],
childGroups: [],
}
]
//load data into database
await prepareTest(unitDatakW, conversionDatakW, meterDatakWGroups, groupDatakW);
//get unit ID since the DB could use any value.
const unitId = await getUnitId('kW');
const expected = [2380.19267225989, 2549.69370712913];
//for compare, need the unitID, currentStart, currentEnd, shift
const res = await chai.request(app).get(`/api/compareReadings/groups/${GROUP_ID}`)
.query({
curr_start: '2022-10-30 00:00:00',
curr_end: '2022-10-31 00:00:00',
shift: 'P1D',
graphicUnitId: unitId,
});
expectCompareToEqualExpected(res, expected, GROUP_ID);
})
// Add CG17 here

// Add CG18 here

// Add CG19 here

mocha.it('CG20: 28 day shift end 2022-10-31 17:12:34 (partial hour) for 15 minute reading intervals and flow units & kW as kW', async () => {

const unitData = [
{
// u4
name: 'kW',
identifier: '',
unitRepresent: Unit.unitRepresentType.FLOW,
secInRate: 3600,
typeOfUnit: Unit.unitType.UNIT,
suffix: '',
displayable: Unit.displayableType.ALL,
preferredDisplay: true,
note: 'kilowatts',
},

{

// u5
name: 'Electric',
identifier: '',
unitRepresent: Unit.unitRepresentType.FLOW,
secInRate: 3600,
typeOfUnit: Unit.unitType.METER,
suffix: '',
displayable: Unit.displayableType.NONE,
preferredDisplay: false,
note: 'special unit'

}
];

const conversionDatakW = [
{
// c4
sourceName: 'Electric',
destinationName: 'kW',
bidirectional: false,
slope: 1,
intercept: 0,
note: 'Electric → kW'

}
];

const meterDatakWGroups = [
{
name: 'meterDatakW',
unit: 'Electric',
defaultGraphicUnit: 'kW',
displayable: true,
gps: undefined,
note: 'special meter',
file: 'test/web/readingsData/readings_ri_15_days_75.csv',
deleteFile: false,
readingFrequency: '15 minutes',
id: METER_ID
},
{
name: 'meterDatakWOther',
unit: 'Electric',
defaultGraphicUnit: 'kW',
displayable: true,
gps: undefined,
note: 'special meter',
file: 'test/web/readingsData/readings_ri_20_days_75.csv',
deleteFile: false,
readingFrequency: '20 minutes',
id: (METER_ID + 1)
}
];

const groupDatakW = [
{
id: GROUP_ID,
name: 'meterDatakW + meterDatakWOther',
displayable: true,
note: 'special group',
defaultGraphicUnit: 'kW',
childMeters: ['meterDatakW', 'meterDatakWOther'],
childGroups: []
}
];

// load data into database
await prepareTest(unitData, conversionDatakW, meterDatakWGroups, groupDatakW);

// get unit ID because DB can use any value
const unitId = await getUnitId('kW');
const expected = [54294.7361355451, 54544.4808583878];

// make API call to get results
const res = await chai.request(app).get(`/api/compareReadings/groups/${GROUP_ID}`)
.query({
curr_start: '2022-10-09 00:00:00',
curr_end: '2022-10-31 17:12:34',
shift: 'P28D',
graphicUnitId: unitId,
});

// compare expected and res
expectCompareToEqualExpected(res, expected, GROUP_ID);
});


// Add CG20 here
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
reading,start time,end time
57744.5416303119,2022-08-23 00:00:00,2022-08-30 00:00:00
58457.1956496669,2022-08-30 00:00:00,2022-09-06 00:00:00
59276.647957841,2022-09-06 00:00:00,2022-09-13 00:00:00
58879.1789750532,2022-09-13 00:00:00,2022-09-20 00:00:00
58647.346330365,2022-09-20 00:00:00,2022-09-27 00:00:00
58324.4920477014,2022-09-27 00:00:00,2022-10-04 00:00:00
58177.8818425628,2022-10-04 00:00:00,2022-10-11 00:00:00
58573.7982237878,2022-10-11 00:00:00,2022-10-18 00:00:00
58642.2652124796,2022-10-18 00:00:00,2022-10-25 00:00:00
58818.8114100034,2022-10-25 00:00:00,2022-11-01 00:00:00
reading,start time,end time
57744.5416303119,2022-08-23 00:00:00,2022-08-30 00:00:00
58457.1956496669,2022-08-30 00:00:00,2022-09-06 00:00:00
59276.647957841,2022-09-06 00:00:00,2022-09-13 00:00:00
58879.1789750532,2022-09-13 00:00:00,2022-09-20 00:00:00
58647.346330365,2022-09-20 00:00:00,2022-09-27 00:00:00
58324.4920477014,2022-09-27 00:00:00,2022-10-04 00:00:00
58177.8818425628,2022-10-04 00:00:00,2022-10-11 00:00:00
58573.7982237878,2022-10-11 00:00:00,2022-10-18 00:00:00
58642.2652124796,2022-10-18 00:00:00,2022-10-25 00:00:00
58818.8114100034,2022-10-25 00:00:00,2022-11-01 00:00:00
Loading
Loading