TASK22019: phase 9 harden file handlers hash guards and negative paths

This commit is contained in:
2026-03-13 12:17:51 +00:00
parent 51269db849
commit c73313b0fa
7 changed files with 340 additions and 75 deletions
+3 -3
View File
@@ -134,7 +134,7 @@ export const uploadFiles = async (
const config = { const config = {
method: "post", method: "post",
url: queryUrl, url: queryUrl + hashAPIPath(queryUrl),
data: formData, data: formData,
headers: { "content-type": "multipart/form-data" } headers: { "content-type": "multipart/form-data" }
}; };
@@ -162,7 +162,7 @@ export const uploadSingleFile = async (filesObj, containerID, casefolderID) => {
const config = { const config = {
method: "post", method: "post",
url: queryUrl, url: queryUrl + hashAPIPath(queryUrl),
data: formData, data: formData,
headers: { "content-type": "multipart/form-data" } headers: { "content-type": "multipart/form-data" }
}; };
@@ -191,7 +191,7 @@ export const uploadRepFiles = async (
const config = { const config = {
method: "post", method: "post",
url: queryUrl, url: queryUrl + hashAPIPath(queryUrl),
data: formData, data: formData,
headers: { "content-type": "multipart/form-data" } headers: { "content-type": "multipart/form-data" }
}; };
+3 -1
View File
@@ -280,7 +280,7 @@ export const sendRepCompleteMessage = async (
caseReference, caseReference,
fileName fileName
) => { ) => {
var queryUrl = var hashQueryPath =
"/api/file/createrepcompletemessage_api?container=" + "/api/file/createrepcompletemessage_api?container=" +
containerID + containerID +
"&tempcaseref=" + "&tempcaseref=" +
@@ -288,6 +288,8 @@ export const sendRepCompleteMessage = async (
"&repid=" + "&repid=" +
fileName; fileName;
var queryUrl = hashQueryPath + hashAPIPath(hashQueryPath);
var config = { var config = {
method: "get", method: "get",
url: queryUrl url: queryUrl
+34 -17
View File
@@ -2,6 +2,8 @@ import {
getBlobs, getBlobs,
createRepCompleteMessage createRepCompleteMessage
} from "../../../actions/azurestorage"; } from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
import { consoleLogger } from "../../../actions/core/logger";
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
@@ -13,26 +15,41 @@ ApiProxy.get(async (req, res) => {
var containerName = req.query.container; var containerName = req.query.container;
var tempCaseRef = req.query.tempcaseref; var tempCaseRef = req.query.tempcaseref;
var filename = req.query.repid; var filename = req.query.repid;
var checkHash = req.query.hash;
//console.log(hashAPIPath(checkquerypath), checkHash); if (
//console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); typeof containerName === "undefined" ||
containerName.length === 0 ||
typeof tempCaseRef === "undefined" ||
tempCaseRef.length === 0 ||
typeof filename === "undefined" ||
filename.length === 0 ||
typeof checkHash === "undefined" ||
checkHash.length === 0
) {
return res.status(400).json();
}
//if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { var checkquerypath =
await createRepCompleteMessage(containerName, tempCaseRef, filename).then( "/api/file/createrepcompletemessage_api?container=" +
(data) => { containerName +
console.log( "&tempcaseref=" +
"/////Create Rep complete Message:\n" + tempCaseRef, tempCaseRef +
"\n" + "insertedOn:" + data.insertedOn, "&repid=" +
"\n" + "messageId:" + data.messageId, filename;
"\n" + "response.status:" + data._response.status,
"\n//////////////" if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
); return res.status(400).json();
}
await createRepCompleteMessage(containerName, tempCaseRef, filename)
.then((data) => {
return res.status(200).json(data); return res.status(200).json(data);
} })
); .catch((error) => {
// } else { consoleLogger(error);
// return res.status(400).json(); return res.status(400).json();
// } });
}); });
export const config = { export const config = {
+10 -8
View File
@@ -17,10 +17,17 @@ ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => { ApiProxy.get(async (req, res) => {
var containerName = req.query.ident; var containerName = req.query.ident;
var checkHash = req.query.hash; var checkHash = req.query.hash;
var checkquerypath = "/api/file/setupcontainer?ident=" + containerName;
//console.log(checkquerypath, hashAPIPath(checkquerypath), checkHash); if (
//console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); typeof containerName === "undefined" ||
containerName.length === 0 ||
typeof checkHash === "undefined" ||
checkHash.length === 0
) {
return res.status(400).json();
}
var checkquerypath = "/api/file/setupcontainer?ident=" + containerName;
if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) {
await createContainer(containerName) await createContainer(containerName)
@@ -32,11 +39,6 @@ ApiProxy.get(async (req, res) => {
res.status(400).json(error); res.status(400).json(error);
}); });
} else { } else {
consoleLogger({
name: "setupcontainer",
code: "bad hash",
query: JSON.stringify(req.query)
});
return res.status(400).json(); return res.status(400).json();
} }
}); });
+20 -28
View File
@@ -1,8 +1,9 @@
import { import {
createBlob, createBlob,
createRepBlob, createRepBlob,
uploadFile, uploadFile
} from "../../../actions/azurestorage"; } from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
@@ -12,26 +13,26 @@ ApiProxy.use(middleware);
ApiProxy.post(async (req, res) => { ApiProxy.post(async (req, res) => {
var checkHash = req.query.hash; var checkHash = req.query.hash;
//console.log(JSON.stringify(req.body));
//console.log(JSON.stringify(req.body.appealData)); var checkquerypath = "/api/file/upload";
//console.log(req.files);
if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
return res.status(400).json();
}
const appealData = req.body.appealData; const appealData = req.body.appealData;
const containerID = req.body.containerID[0]; const containerID = req.body?.containerID?.[0];
const casefolderID = req.body.casefolderID[0]; const casefolderID = req.body?.casefolderID?.[0];
const repOrAppeal = req.body.repOrAppeal || false; const repOrAppeal = req.body.repOrAppeal || false;
console.log("there are files:", Object.keys(req.files).length); if (
typeof containerID === "undefined" ||
// var checkquerypath = "/api/file/upload"; containerID.length === 0 ||
typeof casefolderID === "undefined" ||
//console.log(hashAPIPath(checkquerypath), checkHash); casefolderID.length === 0
//console.log(hashAPIPath(checkquerypath) == "?hash=" + checkHash); ) {
return res.status(400).json();
// if (hashAPIPath(checkquerypath) == "?hash=" + checkHash) { }
//createContainer(containerID).then((containerName) => {
console.log("does this get folder name:", containerID, casefolderID);
repOrAppeal repOrAppeal
? createRepBlob(appealData, containerID, casefolderID).then((data) => { ? createRepBlob(appealData, containerID, casefolderID).then((data) => {
@@ -41,7 +42,6 @@ ApiProxy.post(async (req, res) => {
// return res.status(200).json({ data }); // return res.status(200).json({ data });
// } // }
// ); // );
console.log("================================\nRepfile updated");
return res.status(200).json({ data }); return res.status(200).json({ data });
}) })
: createBlob(appealData, containerID, casefolderID).then((data) => { : createBlob(appealData, containerID, casefolderID).then((data) => {
@@ -51,22 +51,14 @@ ApiProxy.post(async (req, res) => {
// return res.status(200).json({ data }); // return res.status(200).json({ data });
// } // }
// ); // );
console.log(
"================================\nAppeal file updated"
);
return res.status(200).json({ data }); return res.status(200).json({ data });
}); });
//});
// } else {
// return res.status(400).json();
// }
}); });
export const config = { export const config = {
api: { api: {
bodyParser: false, bodyParser: false
}, }
}; };
export default ApiProxy; export default ApiProxy;
+20 -18
View File
@@ -7,6 +7,7 @@ import {
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
import { consoleLogger } from "../../../actions/core/logger"; import { consoleLogger } from "../../../actions/core/logger";
import { hashAPIPath } from "../../../actions/core/hash";
import { fileTypeFromBuffer } from "file-type"; import { fileTypeFromBuffer } from "file-type";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
@@ -36,11 +37,25 @@ ApiProxy.use(middleware);
ApiProxy.post(async (req, res) => { ApiProxy.post(async (req, res) => {
var checkHash = req.query.hash; var checkHash = req.query.hash;
var checkquerypath = "/api/file/uploadsinglefile";
const containerID = req.body.containerID[0]; if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
const casefolderID = req.body.casefolderID[0]; return res.status(400).json();
}
console.log("there are files:", Object.keys(req.files).length); const containerID = req.body?.containerID?.[0];
const casefolderID = req.body?.casefolderID?.[0];
if (
typeof containerID === "undefined" ||
containerID.length === 0 ||
typeof casefolderID === "undefined" ||
casefolderID.length === 0
) {
return res.status(400).json();
}
const uploadedFiles = req.files || {};
// Add other mimetypes here // Add other mimetypes here
const allowedMimeTypes = [ const allowedMimeTypes = [
@@ -57,7 +72,7 @@ ApiProxy.post(async (req, res) => {
var allowedFilesFormData = {}; var allowedFilesFormData = {};
var invalidFiles = []; // To store the names of invalid files var invalidFiles = []; // To store the names of invalid files
for (const [fileName, fileDetails] of Object.entries(req.files)) { for (const [fileName, fileDetails] of Object.entries(uploadedFiles)) {
const file = fileDetails[0]; const file = fileDetails[0];
const filePath = file.path; const filePath = file.path;
@@ -65,9 +80,6 @@ ApiProxy.post(async (req, res) => {
const fnCheck = validateFilenameServer(file.originalFilename); const fnCheck = validateFilenameServer(file.originalFilename);
if (!fnCheck.ok) { if (!fnCheck.ok) {
console.log(
`File ${file.originalFilename} rejected: ${fnCheck.reason}`
);
invalidFiles.push(`${file.originalFilename} - Invalid filename`); invalidFiles.push(`${file.originalFilename} - Invalid filename`);
continue; // do not process further continue; // do not process further
} }
@@ -80,7 +92,6 @@ ApiProxy.post(async (req, res) => {
try { try {
const type = await fileTypeFromBuffer(buffer); // Correct usage of fileTypeFromBuffer const type = await fileTypeFromBuffer(buffer); // Correct usage of fileTypeFromBuffer
console.log("checking mime type ", type);
if (type && allowedMimeTypes.includes(type.mime)) { if (type && allowedMimeTypes.includes(type.mime)) {
// If the MIME type from the file signature matches the allowed list // If the MIME type from the file signature matches the allowed list
allowedFilesFormData[fileName] = fileDetails.map( allowedFilesFormData[fileName] = fileDetails.map(
@@ -94,21 +105,12 @@ ApiProxy.post(async (req, res) => {
}) })
); );
} else { } else {
console.log(
`\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\nFile ${fileName} has an invalid MIME type based on its content.\n\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\`
);
invalidFiles.push(fileName); // Track invalid file invalidFiles.push(fileName); // Track invalid file
} }
} catch (error) { } catch (error) {
console.log( consoleLogger(error);
`Error reading file ${fileName} for MIME type validation`,
error
);
} }
} else { } else {
console.log(
`\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\nFile ${fileName} has an unsupported MIME type: ${fileMimeType}\n\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\`
);
invalidFiles.push(fileName); // Track invalid file invalidFiles.push(fileName); // Track invalid file
} }
} }
+250
View File
@@ -0,0 +1,250 @@
const fs = require("fs");
const path = require("path");
const vm = require("vm");
const assert = require("assert");
const rootDir = path.resolve(__dirname, "..", "..");
const loadModule = (relativePath, injected = {}) => {
const filePath = path.join(rootDir, relativePath);
let source = fs.readFileSync(filePath, "utf8");
source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, "");
source = source.replace(
/export default async function\s+(\w+)\s*\(/,
"async function $1("
);
source = source.replace(/export const\s+/g, "const ");
source = source.replace(
/export default\s+(\w+);/g,
"module.exports.default = $1;"
);
source +=
'\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n';
const context = {
module: { exports: {} },
exports: {},
require,
process,
console: {
log: () => {},
info: () => {},
warn: () => {},
error: () => {}
},
...injected
};
vm.runInNewContext(source, context, { filename: filePath });
return context.module.exports;
};
const createNextConnectMock = () => {
const router = {
handler: null,
use: () => {},
get(fn) {
this.handler = fn;
},
post(fn) {
this.handler = fn;
}
};
return () => router;
};
const createRes = () => {
const state = {
statusCode: null,
jsonBody: undefined
};
return {
state,
status(code) {
state.statusCode = code;
return this;
},
json(payload) {
state.jsonBody = payload;
return payload;
}
};
};
const tests = [];
const test = (name, fn) => tests.push({ name, fn });
test("createrepcompletemessage_api rejects missing hash with 400", async () => {
const calls = [];
const nextConnect = createNextConnectMock();
const mod = loadModule("pages/api/file/createrepcompletemessage_api.js", {
hashAPIPath: () => "&hash=expected",
consoleLogger: () => {},
createRepCompleteMessage: async (...args) => {
calls.push(args);
return { ok: true };
},
nextConnect,
middleware: () => {}
});
const req = {
query: {
container: "c1",
tempcaseref: "temp-1",
repid: "rep-1"
}
};
const res = createRes();
await mod.default.handler(req, res);
assert.strictEqual(res.state.statusCode, 400);
assert.strictEqual(calls.length, 0);
});
test("upload rejects missing hash with 400", async () => {
const blobCalls = [];
const nextConnect = createNextConnectMock();
const mod = loadModule("pages/api/file/upload.js", {
hashAPIPath: () => "?hash=expected",
createBlob: async (...args) => {
blobCalls.push(args);
return { ok: true };
},
createRepBlob: async (...args) => {
blobCalls.push(args);
return { ok: true };
},
uploadFile: async () => {},
nextConnect,
middleware: () => {}
});
const req = {
query: {},
body: {
appealData: {},
containerID: ["c1"],
casefolderID: ["case-1"]
},
files: {}
};
const res = createRes();
await mod.default.handler(req, res);
assert.strictEqual(res.state.statusCode, 400);
assert.strictEqual(blobCalls.length, 0);
});
test("uploadsinglefile rejects invalid hash with 400", async () => {
const uploadCalls = [];
const nextConnect = createNextConnectMock();
const mod = loadModule("pages/api/file/uploadsinglefile.js", {
hashAPIPath: () => "?hash=expected",
uploadSingleFile: async (...args) => {
uploadCalls.push(args);
return { ok: true };
},
consoleLogger: () => {},
fileTypeFromBuffer: async () => ({ mime: "application/pdf" }),
fs: { readFileSync: () => Buffer.from("file") },
path: { basename: (value) => value },
nextConnect,
middleware: () => {}
});
const req = {
query: { hash: "wrong" },
body: { containerID: ["c1"], casefolderID: ["case-1"] },
files: {}
};
const res = createRes();
await mod.default.handler(req, res);
assert.strictEqual(res.state.statusCode, 400);
assert.strictEqual(uploadCalls.length, 0);
});
test("setupcontainer rejects missing ident with 400", async () => {
const createCalls = [];
const nextConnect = createNextConnectMock();
const mod = loadModule("pages/api/file/setupcontainer.js", {
hashAPIPath: () => "&hash=expected",
createContainer: async (...args) => {
createCalls.push(args);
return { ok: true };
},
createContainerSas: async () => {},
getContainers: async () => {},
getBlobs: async () => {},
uploadFile: async () => {},
consoleLogger: () => {},
nextConnect,
middleware: () => {}
});
const req = { query: { hash: "expected" } };
const res = createRes();
await mod.default.handler(req, res);
assert.strictEqual(res.state.statusCode, 400);
assert.strictEqual(createCalls.length, 0);
});
test("setupcontainer rejects invalid hash with 400", async () => {
const createCalls = [];
const nextConnect = createNextConnectMock();
const mod = loadModule("pages/api/file/setupcontainer.js", {
hashAPIPath: () => "&hash=expected",
createContainer: async (...args) => {
createCalls.push(args);
return { ok: true };
},
createContainerSas: async () => {},
getContainers: async () => {},
getBlobs: async () => {},
uploadFile: async () => {},
consoleLogger: () => {},
nextConnect,
middleware: () => {}
});
const req = { query: { ident: "container-1", hash: "wrong" } };
const res = createRes();
await mod.default.handler(req, res);
assert.strictEqual(res.state.statusCode, 400);
assert.strictEqual(createCalls.length, 0);
});
const run = async () => {
let passed = 0;
for (const currentTest of tests) {
await currentTest.fn();
passed += 1;
}
console.log(
`Phase 9 behavioural tests passed (${passed}/${tests.length}).`
);
};
run().catch((error) => {
console.error(error);
process.exit(1);
});