Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions lib/dlgBulk.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

*/

const pathSecurity = require('./pathSecurity.js')

function bulkOps(pid, dest, automode){
if(dlgBulk!==null){
dlgFactory.close(dlgBulk)
Expand Down Expand Up @@ -167,6 +169,32 @@ function bulkOps(pid, dest, automode){
alert(`Please select items to ${type}.`)
return
}

if((type=='Copy' || type=='Move') && dest) {
var destValidation = pathSecurity.validatePath(dest, `${type.toLowerCase()} destination`)
if(destValidation.valid === false) {
console.error('dlgBulk.exec() destination validation failed:', destValidation.error)
alert(`Security Error: ${destValidation.error}`)
return
}
dest = destValidation.path
}

for(let obj of list) {
if(!obj || !obj.path) {
console.error('dlgBulk.exec() error: invalid object in list')
alert('Security Error: Invalid object in operation list.')
return
}

var sourceValidation = pathSecurity.validatePath(obj.path, `${type.toLowerCase()} source`)
if(sourceValidation.valid === false) {
console.error('dlgBulk.exec() source validation failed:', sourceValidation.error)
alert(`Security Error: ${sourceValidation.error}`)
return
}
obj.path = sourceValidation.path
}

let automode = dlgBulk.querySelector('#cbBulkAutoMode').checked
//console.log('dlgBulk.automode', automode)
Expand Down
40 changes: 39 additions & 1 deletion lib/dlgRename.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

*/
var dlgRename = null
const pathSecurity = require('./pathSecurity.js')

function dlgRenameCreate(controlId) {
if(dlgRename != null) return
Expand Down Expand Up @@ -641,14 +642,51 @@ function initUiVars(){
return
}

if(newfn.includes('/') || newfn.includes('\\')) {
dlgFactory.bbl(`Filename cannot contain path separators.`)
console.error('dlgRenameBtnRenameClick() error: filename contains path separators')
alert('Security Error: Filename cannot contain path separators.')
return
}

if(newfn.includes('..')) {
dlgFactory.bbl(`Filename contains potentially dangerous characters.`)
console.error('dlgRenameBtnRenameClick() error: filename contains potentially dangerous characters')
alert('Security Error: Filename contains potentially dangerous characters.')
return
}

const path = require('path')
const fs = require('fs')
let obj = dlgRename.dlg.obj //dlg.dlg.obj

if(!obj || !obj.path) {
console.error('dlgRenameBtnRenameClick() error: invalid object')
alert('Security Error: Invalid object for rename operation.')
return
}

var sourceValidation = pathSecurity.validatePath(obj.path, 'rename source')
if(sourceValidation.valid === false) {
console.error('dlgRenameBtnRenameClick() source validation failed:', sourceValidation.error)
alert(`Security Error: ${sourceValidation.error}`)
return
}
obj.path = sourceValidation.path

var fn = path.basename(obj.path)
var ext = path.extname(obj.path)

let parentDir = path.dirname(obj.path)
let childValidation = pathSecurity.validateChildPath(parentDir, newfn, 'rename')
if(childValidation.valid === false) {
console.error('dlgRenameBtnRenameClick() child validation failed:', childValidation.error)
alert(`Security Error: ${childValidation.error}`)
return
}

let oldpath = ui.calc.pathForOS(obj.path),
newpath = path.join(path.dirname(obj.path), '/', newfn)
newpath = childValidation.fullPath
if(obj.isDirectory) newpath += '\\'
if(fs.existsSync(newpath)===true){
dlgFactory.bbl(`An item named "${newfn}" was found in this folder.`)
Expand Down
15 changes: 14 additions & 1 deletion lib/pathBar.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
function pathBar(){
const pathSecurity = require('./pathSecurity.js')

function pathBar(){
this.dlg=null
this.btnResult = false
this.dlgfolderitems = null
Expand Down Expand Up @@ -240,6 +242,15 @@
this.folderLoad = function(url, msg, showitems){
//console.log( 'folderLoad', url)
// let _start = ui.calc.timeStart()

var pathValidation = pathSecurity.validatePath(url, 'pathBar folder load')
if(pathValidation.valid === false) {
console.error('pathBar.folderLoad() path validation failed:', pathValidation.error)
alert(`Security Error: ${pathValidation.error}`)
return
}
url = pathValidation.path

var data = renderer.pathFolderItemsLoad(url, true)
if(typeof data === 'string'){
console.log(`folderLoad() error for: [${url}] \nMessage: ${data}`)
Expand All @@ -249,6 +260,8 @@

ui.var.pathBarPathLast = ui.var.pathBarPath
ui.var.pathBarPath = data.fldr

pathSecurity.addRootPath(data.fldr)

var dlg = this.dlg
var folderitems = this.folderItemsSort( data.items ) //sort by basename
Expand Down
240 changes: 240 additions & 0 deletions lib/pathSecurity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
"use strict";

const path = require('path');
const fs = require('fs');

var pathSecurity = {
rootPaths: [],

init: function() {
this.rootPaths = [];
},

addRootPath: function(rootPath) {
if (!rootPath) return false;

rootPath = this.normalizePath(rootPath);

if (!rootPath) return false;

if (this.rootPaths.indexOf(rootPath) === -1) {
this.rootPaths.push(rootPath);
}
return true;
},

setRootPaths: function(paths) {
this.rootPaths = [];
if (Array.isArray(paths)) {
for (let p of paths) {
this.addRootPath(p);
}
}
},

getRootPaths: function() {
return this.rootPaths.slice();
},

normalizePath: function(p) {
if (!p) return '';

p = p.trim();

p = p.replace(/\\/g, '/');

if (p.length >= 8 && p.substring(0, 8) === 'file:///') {
p = p.substring(8);
}

if (p === '') return '';

try {
p = path.normalize(p);
} catch(e) {
console.error('pathSecurity.normalizePath() error:', e);
return '';
}

p = p.replace(/\\/g, '/');

if (p.length > 0 && p[p.length - 1] !== '/') {
try {
let stat = null;
try {
stat = fs.lstatSync(p);
} catch(e) {}

if (stat && stat.isDirectory()) {
p += '/';
}
} catch(e) {}
}

return p;
},

containsPathTraversal: function(p) {
if (!p) return false;

p = this.normalizePath(p);
if (!p) return false;

if (p.indexOf('../') >= 0 || p.indexOf('..\\') >= 0) {
return true;
}

if (p.includes('/./') || p.includes('\\.\\')) {
return true;
}

if (p.includes('~') || p.includes('$')) {
return true;
}

return false;
},

isPathUnderRoot: function(targetPath, rootPath) {
if (!targetPath || !rootPath) return false;

targetPath = this.normalizePath(targetPath);
rootPath = this.normalizePath(rootPath);

if (!targetPath || !rootPath) return false;

if (targetPath === rootPath) return true;

if (rootPath[rootPath.length - 1] !== '/') {
rootPath += '/';
}

return targetPath.startsWith(rootPath);
},

isPathUnderAnyRoot: function(targetPath) {
if (!targetPath) return false;

if (this.rootPaths.length === 0) {
return true;
}

for (let root of this.rootPaths) {
if (this.isPathUnderRoot(targetPath, root)) {
return true;
}
}
return false;
},

validatePath: function(targetPath, operation = 'operation') {
let result = {
valid: false,
error: null,
path: null
};

if (!targetPath) {
result.error = `Path is required for ${operation}.`;
return result;
}

let normalizedPath = this.normalizePath(targetPath);
if (!normalizedPath) {
result.error = `Invalid path format for ${operation}.`;
return result;
}

result.path = normalizedPath;

if (this.containsPathTraversal(targetPath)) {
result.error = `Path contains potentially dangerous characters for ${operation}.`;
return result;
}

if (!this.isPathUnderAnyRoot(normalizedPath)) {
result.error = `Access denied for ${operation}. Path is outside allowed directories.`;
return result;
}

result.valid = true;
return result;
},

validateChildPath: function(parentPath, childName, operation = 'operation') {
let result = {
valid: false,
error: null,
fullPath: null
};

if (!parentPath || !childName) {
result.error = `Parent path and child name are required for ${operation}.`;
return result;
}

parentPath = this.normalizePath(parentPath);
if (!parentPath) {
result.error = `Invalid parent path for ${operation}.`;
return result;
}

childName = childName.trim();

if (childName === '' || childName === '.' || childName === '..') {
result.error = `Invalid child name for ${operation}.`;
return result;
}

if (childName.includes('/') || childName.includes('\\')) {
result.error = `Child name cannot contain path separators for ${operation}.`;
return result;
}

if (childName.includes('..')) {
result.error = `Child name contains potentially dangerous characters for ${operation}.`;
return result;
}

let fullPath = path.join(parentPath, childName);
fullPath = this.normalizePath(fullPath);

if (!this.isPathUnderRoot(fullPath, parentPath)) {
result.error = `Resulting path is outside parent directory for ${operation}.`;
return result;
}

if (!this.isPathUnderAnyRoot(fullPath)) {
result.error = `Resulting path is outside allowed directories for ${operation}.`;
return result;
}

result.fullPath = fullPath;
result.valid = true;
return result;
},

validateMoveOperation: function(sourcePath, destPath, operation = 'move') {
let result = {
valid: false,
error: null
};

let sourceValidation = this.validatePath(sourcePath, `${operation} source`);
if (!sourceValidation.valid) {
result.error = sourceValidation.error;
return result;
}

let destValidation = this.validatePath(destPath, `${operation} destination`);
if (!destValidation.valid) {
result.error = destValidation.error;
return result;
}

result.valid = true;
return result;
}
};

module.exports = pathSecurity;
Loading