Skip to content

Commit d1ff193

Browse files
committed
fix for reported vulnerability in restore process - CVE still being issued
1 parent df4cfd2 commit d1ff193

2 files changed

Lines changed: 94 additions & 38 deletions

File tree

backend/routes/backup.js

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,30 @@ const { sendUpdate } = require("../ws");
1616
const router = express.Router();
1717
const TaskManager = require("../classes/task-manager-singleton");
1818
const TaskScheduler = require("../classes/task-scheduler-singleton");
19+
const { tables } = require("../global/backup_tables");
1920

2021
// Database connection parameters
2122
const postgresUser = process.env.POSTGRES_USER;
2223
const postgresPassword = process.env.POSTGRES_PASSWORD;
2324
const postgresIp = process.env.POSTGRES_IP;
2425
const postgresPort = process.env.POSTGRES_PORT;
2526
const postgresDatabase = process.env.POSTGRES_DB || "jfstat";
26-
const postgresSslRejectUnauthorized = process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true";
27+
const postgresSslRejectUnauthorized =
28+
process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true";
2729

2830
const backupfolder = "backup-data";
2931

32+
// table mappers
33+
const jf_libraries = require("../models/jf_libraries");
34+
const jf_library_items = require("../models/jf_library_items");
35+
const jf_library_seasons = require("../models/jf_library_seasons");
36+
const jf_library_episodes = require("../models/jf_library_episodes");
37+
const jf_users = require("../models/jf_users");
38+
const jf_playback_activity = require("../models/jf_playback_activity");
39+
const jf_playback_reporting_plugin_data = require("../models/jf_playback_reporting_plugin_data");
40+
const jf_item_info = require("../models/jf_item_info");
41+
//
42+
const db = require("../db");
3043
// Restore function
3144

3245
function readFile(path) {
@@ -63,16 +76,49 @@ function getBirthtimeFallback(fileStats, fileName) {
6376
// 2digits('-' or ':')2digits('-' or ':')2digits
6477
const regexp = /(\d{4})-(\d{2})-(\d{2})[ _T](\d{2})[-:](\d{2})[-:](\d{2})/;
6578
const matches = fileName.match(regexp);
66-
if (!matches)
67-
return null;
79+
if (!matches) return null;
6880

6981
// Verify that each regex match is a valid number
70-
for (var i=1; i<7; i++) {
71-
if (Number.isNaN(Number(matches[i])))
82+
for (var i = 1; i < 7; i++) {
83+
if (Number.isNaN(Number(matches[i]))) return null;
84+
}
85+
86+
return new Date(matches[1], matches[2] - 1, matches[3], matches[4], matches[5], matches[6]);
87+
}
88+
89+
function getTableColumns(tableName) {
90+
switch (tableName) {
91+
case "jf_libraries":
92+
return jf_libraries.jf_libraries_columns;
93+
case "jf_library_items":
94+
return jf_library_items.jf_library_items_columns;
95+
case "jf_library_seasons":
96+
return jf_library_seasons.jf_library_seasons_columns;
97+
case "jf_library_episodes":
98+
return jf_library_episodes.jf_library_episodes_columns;
99+
case "jf_users":
100+
return jf_users.jf_users_columns;
101+
case "jf_playback_activity":
102+
return jf_playback_activity.columnsPlayback;
103+
case "jf_playback_reporting_plugin_data":
104+
return jf_playback_reporting_plugin_data.columnsPlaybackReporting;
105+
case "jf_item_info":
106+
return jf_item_info.jf_item_info_columns;
107+
default:
72108
return null;
73109
}
110+
}
74111

75-
return new Date(matches[1], matches[2]-1, matches[3], matches[4], matches[5], matches[6]);
112+
//fixes Bulk insert error: error: column "Genres" is of type jsonb but expression is of type text[]
113+
function formatData(data) {
114+
if (!Array.isArray(data)) return data;
115+
return data.map((row) => {
116+
const formatted = {};
117+
for (const [key, value] of Object.entries(row)) {
118+
formatted[key] = Array.isArray(value) ? JSON.stringify(value) : value;
119+
}
120+
return formatted;
121+
});
76122
}
77123

78124
async function restore(file, refLog) {
@@ -81,17 +127,6 @@ async function restore(file, refLog) {
81127
color: "yellow",
82128
Message: "Restoring from Backup: " + file,
83129
});
84-
const pool = new Pool({
85-
user: postgresUser,
86-
password: postgresPassword,
87-
host: postgresIp,
88-
port: postgresPort,
89-
database: postgresDatabase,
90-
...(process.env.POSTGRES_SSL_ENABLED === "true"
91-
? { ssl: { rejectUnauthorized: postgresSslRejectUnauthorized } }
92-
: {}),
93-
});
94-
95130
const backupPath = file;
96131

97132
let jsonData;
@@ -102,7 +137,6 @@ async function restore(file, refLog) {
102137
} catch (err) {
103138
refLog.logData.push({
104139
color: "red",
105-
key: tableName,
106140
Message: `Failed to read backup file`,
107141
});
108142
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
@@ -114,38 +148,60 @@ async function restore(file, refLog) {
114148
console.log("No Data");
115149
return;
116150
}
151+
const allowList = tables.map((table) => table.value);
117152

153+
// Perform bulk, parameterized inserts per table to improve performance
118154
for (let table of jsonData) {
119155
const data = Object.values(table)[0];
120156
const tableName = Object.keys(table)[0];
157+
158+
if (!allowList.includes(tableName)) {
159+
refLog.logData.push({
160+
color: "red",
161+
Message: `Table ${tableName} is not allowed to be restored`,
162+
});
163+
continue;
164+
}
165+
166+
const tableColumns = getTableColumns(tableName);
167+
if (!tableColumns) {
168+
refLog.logData.push({
169+
color: "red",
170+
Message: `No columns found for table ${tableName}`,
171+
});
172+
continue;
173+
}
174+
121175
refLog.logData.push({
122176
color: "dodgerblue",
123177
key: tableName,
124178
Message: `Restoring ${tableName}`,
125179
});
126-
for (let index in data) {
127-
const keysWithQuotes = Object.keys(data[index]).map((key) => `"${key}"`);
128-
const keyString = keysWithQuotes.join(", ");
129-
130-
const valuesWithQuotes = Object.values(data[index]).map((col) => {
131-
if (col === null) {
132-
return "NULL";
133-
} else if (typeof col === "string") {
134-
return `'${col.replace(/'/g, "''")}'`;
135-
} else if (typeof col === "object") {
136-
return `'${JSON.stringify(col).replace(/'/g, "''")}'`;
137-
} else {
138-
return `'${col}'`;
139-
}
140-
});
141180

142-
const valueString = valuesWithQuotes.join(", ");
181+
if (data.length == 0) {
182+
refLog.logData.push({
183+
color: "yellow",
184+
key: tableName,
185+
Message: `No data to restore for ${tableName}`,
186+
});
187+
continue;
188+
}
189+
let result = await db.insertBulk(tableName, formatData(data), tableColumns);
143190

144-
const query = `INSERT INTO ${tableName} (${keyString}) VALUES(${valueString}) ON CONFLICT DO NOTHING`;
145-
const { rows } = await pool.query(query);
191+
if (result.Result === "SUCCESS") {
192+
refLog.logData.push({
193+
color: "lawngreen",
194+
key: tableName,
195+
Message: `Restored ${data.length} rows to ${tableName}`,
196+
});
197+
} else {
198+
refLog.logData.push({
199+
color: "red",
200+
key: tableName,
201+
Message: `Failed to restore ${tableName}: ${result.message}`,
202+
});
146203
}
147204
}
148-
await pool.end();
149205
refLog.logData.push({ color: "lawngreen", Message: "Restore Complete" });
150206
}
151207

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jfstat",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"private": true,
55
"main": "src/index.jsx",
66
"scripts": {

0 commit comments

Comments
 (0)