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
35 changes: 35 additions & 0 deletions API/Classes/Base/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from pathlib import Path
from zipfile import ZipFile

class Security:
@staticmethod
def safeCasePath(baseDir, casename):
"""Resolve casename under baseDir and ensure it stays within baseDir"""
base = Path(baseDir).resolve()
target = (base / casename).resolve()

if not str(target).startswith(str(base) + "\\") and target != base:
if not str(target).startswith(str(base) + "/") and target != base:
raise ValueError(
"Path traversal detected: '" + casename + "' escapes base directory"
)

return target

@staticmethod
def safeExtractall(zf, targetDir):
"""Extract ZIP only after verifying no entry escapes targetDir"""
target = Path(targetDir).resolve()

for member in zf.infolist():
memberPath = (target / member.filename).resolve()

if not str(memberPath).startswith(str(target) + "\\") and \
not str(memberPath).startswith(str(target) + "/") and \
memberPath != target:
raise ValueError(
"ZIP slip detected: '" + member.filename + "' would escape target directory"
)

#all entries validated, safe to extract
zf.extractall(str(targetDir))
41 changes: 41 additions & 0 deletions API/Routes/Case/CaseRoute.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pandas as pd
from Classes.Base import Config
from Classes.Base.FileClass import File
from Classes.Base.security import Security
from Classes.Case.CaseClass import Case
from Classes.Case.UpdateCaseClass import UpdateCase
from Classes.Case.ImportTemplate import ImportTemplate
Expand Down Expand Up @@ -43,6 +44,10 @@ def getResultCSV():
try:
casename = request.json['casename']
caserunname = request.json['caserunname']
try:
Security.safeCasePath(Config.DATA_STORAGE, casename)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400
csvFolder = Path(Config.DATA_STORAGE,casename,"res", caserunname, "csv")
if os.path.isdir(csvFolder):
csvs = [ f.name for f in os.scandir(csvFolder) ]
Expand All @@ -56,6 +61,10 @@ def getResultCSV():
def getDesc():
try:
casename = request.json['casename']
try:
Security.safeCasePath(Config.DATA_STORAGE, casename)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400
genDataPath = Path(Config.DATA_STORAGE,casename,"genData.json")
genData = File.readFile(genDataPath)
response = {
Expand All @@ -70,6 +79,10 @@ def getDesc():
def copy():
try:
case = request.json['casename']
try:
Security.safeCasePath(Config.DATA_STORAGE, case)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400
case_copy = case + '_copy'
casePath = Path(Config.DATA_STORAGE, case_copy, 'genData.json')

Expand Down Expand Up @@ -101,6 +114,10 @@ def copy():
def deleteCase():
try:
case = request.json['casename']
try:
Security.safeCasePath(Config.DATA_STORAGE, case)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400

casePath = Path(Config.DATA_STORAGE, case)
shutil.rmtree(casePath)
Expand Down Expand Up @@ -128,6 +145,10 @@ def getResultData():
casename = request.json['casename']
dataJson = request.json['dataJson']
if casename != None:
try:
Security.safeCasePath(Config.DATA_STORAGE, casename)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400
dataPath = Path(Config.DATA_STORAGE,casename,'view',dataJson)
data = File.readFile(dataPath)
response = data
Expand All @@ -142,6 +163,10 @@ def getResultData():
def getParamFile():
try:
dataJson = request.json['dataJson']
try:
Security.safeCasePath(Config.DATA_STORAGE, dataJson)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400
configPath = Path(Config.DATA_STORAGE, dataJson)
ConfigFile = File.readParamFile(configPath)
response = ConfigFile
Expand All @@ -154,6 +179,10 @@ def resultsExists():
try:
casename = request.json['casename']
if casename != None:
try:
Security.safeCasePath(Config.DATA_STORAGE, casename)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400
resPath = Path(Config.DATA_STORAGE, casename, 'view', 'RYT.json')
dataPath = Path(Config.DATA_STORAGE,casename,'view','resData.json')
data = File.readFile(dataPath)
Expand Down Expand Up @@ -196,6 +225,10 @@ def saveScOrder():
try:
data = request.json['data']
case = request.json['casename']
try:
Security.safeCasePath(Config.DATA_STORAGE, case)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400
genDataPath = Path(Config.DATA_STORAGE, case, 'genData.json')
genData = File.readFile(genDataPath)
genData['osy-scenarios'] = data
Expand Down Expand Up @@ -235,6 +268,10 @@ def saveCase():
try:
genData = request.json['data']
casename = genData['osy-casename']
try:
Security.safeCasePath(Config.DATA_STORAGE, casename)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400
case = session.get('osycase', None)

configPath = Path(Config.DATA_STORAGE, 'Variables.json')
Expand Down Expand Up @@ -390,6 +427,10 @@ def saveCase():
def prepareCSV():
try:
casename = request.json['casename']
try:
Security.safeCasePath(Config.DATA_STORAGE, casename)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400
jsonData = request.json['jsonData']

Pd = pd.DataFrame(jsonData)
Expand Down
24 changes: 15 additions & 9 deletions API/Routes/Upload/UploadRoute.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from Classes.Base import Config
from Classes.Base.FileClass import File
from Classes.Base.security import Security

upload_api = Blueprint('UploadRoute', __name__)

Expand Down Expand Up @@ -208,6 +209,11 @@ def backupCase():
#case = request.json['casename']
case = request.args.get('case')

try:
Security.safeCasePath(Config.DATA_STORAGE, case)
except ValueError as e:
return jsonify({'message': str(e), 'status_code': 'error'}), 400

casePath = Path('WebAPP', 'DataStorage',case)
zippedFile = Path('WebAPP', 'DataStorage', case+'.zip')

Expand Down Expand Up @@ -278,7 +284,7 @@ def uploadCaseUnchunked_old():
name = data.get('osy-version', None)

if name == '1.0' or name == '2.0':
zf.extractall(os.path.join(Config.EXTRACT_FOLDER))
Security.safeExtractall(zf, os.path.join(Config.EXTRACT_FOLDER))

#add res view folders with json default files
configPath = Path(Config.DATA_STORAGE, 'Variables.json')
Expand Down Expand Up @@ -325,7 +331,7 @@ def uploadCaseUnchunked_old():
elif name == '3.0':
#potrebno dodati tech groups
#case = data.get('osy-casename', None)
zf.extractall(os.path.join(Config.EXTRACT_FOLDER))
Security.safeExtractall(zf, os.path.join(Config.EXTRACT_FOLDER))
genDataPath = Path(Config.DATA_STORAGE, casename, 'genData.json')
genData = File.readParamFile(genDataPath)
genData["osy-techGroups"] = []
Expand All @@ -344,7 +350,7 @@ def uploadCaseUnchunked_old():
"casename": casename
})
elif name == '4.0' or name == '4.5' or name == '4.9':
zf.extractall(os.path.join(Config.EXTRACT_FOLDER))
Security.safeExtractall(zf, os.path.join(Config.EXTRACT_FOLDER))
# potrebno updatevoati YearSplit u verziji 5.0 su dinamicki
#update for dynamic timeslicec
updateTimeslices(casename)
Expand All @@ -360,7 +366,7 @@ def uploadCaseUnchunked_old():
})

# elif name == '4.9':
# zf.extractall(os.path.join(Config.EXTRACT_FOLDER))
# Security.safeExtractall(zf, os.path.join(Config.EXTRACT_FOLDER))
# # potrebno updatevoati YearSplit u verziji 5.0 su dinamicki
# #update for dynamic timeslicec
# updateTimeslices(casename)
Expand All @@ -372,7 +378,7 @@ def uploadCaseUnchunked_old():
# })

elif name == '5.0':
zf.extractall(os.path.join(Config.EXTRACT_FOLDER))
Security.safeExtractall(zf, os.path.join(Config.EXTRACT_FOLDER))
updateViewDefintions(casename)
msg.append({
"message": "Model " + casename +" have been uploaded!",
Expand Down Expand Up @@ -455,7 +461,7 @@ def handle_full_zip(file, filepath=None):
# TVOJA ORIGINALNA LOGIKA
# ---------------------------
if name == '1.0' or name == '2.0':
zf.extractall(os.path.join(Config.EXTRACT_FOLDER))
Security.safeExtractall(zf, os.path.join(Config.EXTRACT_FOLDER))
configPath = Path(Config.DATA_STORAGE, 'Variables.json')
vars = File.readParamFile(configPath)
viewDef = {}
Expand Down Expand Up @@ -484,7 +490,7 @@ def handle_full_zip(file, filepath=None):
"casename": casename
})
elif name == '3.0':
zf.extractall(os.path.join(Config.EXTRACT_FOLDER))
Security.safeExtractall(zf, os.path.join(Config.EXTRACT_FOLDER))
genDataPath = Path(Config.DATA_STORAGE, casename, 'genData.json')
genData = File.readParamFile(genDataPath)
genData["osy-techGroups"] = []
Expand All @@ -500,7 +506,7 @@ def handle_full_zip(file, filepath=None):
"casename": casename
})
elif name in ['4.0', '4.5', '4.9']:
zf.extractall(os.path.join(Config.EXTRACT_FOLDER))
Security.safeExtractall(zf, os.path.join(Config.EXTRACT_FOLDER))
updateTimeslices(casename)
updateStorageSet(casename)
updateViewDefintions(casename)
Expand All @@ -511,7 +517,7 @@ def handle_full_zip(file, filepath=None):
"casename": casename
})
elif name == '5.0':
zf.extractall(os.path.join(Config.EXTRACT_FOLDER))
Security.safeExtractall(zf, os.path.join(Config.EXTRACT_FOLDER))
updateViewDefintions(casename)
msg.append({
"message": "Model " + casename +" have been uploaded!",
Expand Down
2 changes: 1 addition & 1 deletion API/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

app.permanent_session_lifetime = timedelta(days=5)
app.config['SECRET_KEY'] = '12345'
app.config["MAX_CONTENT_LENGTH"] = None
app.config["MAX_CONTENT_LENGTH"] = 500 * 1024 * 1024 # 500 MB

app.register_blueprint(upload_api)
app.register_blueprint(case_api)
Expand Down