Skip to content

Commit

Permalink
[processing] Replace format for saving batch processing parameters
Browse files Browse the repository at this point in the history
The older approach of storing parameters was insecure, and required
eval()ing the unchecked contents of the batch parameter file. This
is a security risk, as a malicious file could leak user information
or damage the system.

So,

- Switch to a newer ".batch" format which uses safe JSON objects
only for serializing parameter values.
- Allow loading the older .json files, but first show a warning
that they are a security risk and require to user to explicitly
agree to open the file
  • Loading branch information
nyalldawson committed Oct 1, 2024
1 parent 95c9bb5 commit 6c1de7f
Showing 1 changed file with 98 additions and 28 deletions.
126 changes: 98 additions & 28 deletions python/plugins/processing/gui/BatchPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import json
import warnings
from pathlib import Path
from typing import Optional
from typing import Optional, List, Dict

from qgis.PyQt import uic
from qgis.PyQt.QtWidgets import (
Expand Down Expand Up @@ -83,7 +83,8 @@
QgsRasterLayer,
QgsProcessingUtils,
QgsFileFilterGenerator,
QgsProcessingContext
QgsProcessingContext,
QgsFileUtils
)
from qgis.gui import (
QgsProcessingParameterWidgetContext,
Expand Down Expand Up @@ -440,6 +441,9 @@ def populateByExpression(self, adding=False):
class BatchPanel(QgsPanelWidget, WIDGET):
PARAMETERS = "PARAMETERS"
OUTPUTS = "OUTPUTS"
ROWS = "rows"
FORMAT = "format"
CURRENT_FORMAT = "batch_3.40"

def __init__(self, parent, alg):
super().__init__(None)
Expand Down Expand Up @@ -543,22 +547,82 @@ def clear(self):
self.wrappers = []

def load(self):
context = dataobjects.createContext()
settings = QgsSettings()
last_path = settings.value("/Processing/LastBatchPath", QDir.homePath())
filename, selected_filter = QFileDialog.getOpenFileName(self,
self.tr('Open Batch'), last_path,
self.tr('JSON files (*.json)'))
if filename:
last_path = QFileInfo(filename).path()
settings.setValue('/Processing/LastBatchPath', last_path)
with open(filename) as f:
values = json.load(f)
filters = ';;'.join([self.tr('Batch Processing files (*.batch)'),
self.tr('JSON files (*.json)')])
filename, _ = QFileDialog.getOpenFileName(self,
self.tr('Open Batch'),
last_path,
filters)
if not filename:
return

last_path = QFileInfo(filename).path()
settings.setValue('/Processing/LastBatchPath', last_path)
with open(filename) as f:
values = json.load(f)

if isinstance(values, dict):
if values.get(self.FORMAT) == self.CURRENT_FORMAT:
self.load_batch_file_3_40_version(values)
else:
QMessageBox.critical(
self,
self.tr('Load Batch Parameters'),
self.tr('This file format is unknown and cannot be opened as batch parameters.'))
else:
# If the user clicked on the cancel button.
self.load_old_json_batch_file(values)

def load_batch_file_3_40_version(self, values: Dict):
"""
Loads the newer version 3.40 batch parameter JSON format
"""
context = dataobjects.createContext()
rows: List = values.get(self.ROWS, [])

self.clear()
for row_number, row in enumerate(rows):
self.addRow()
this_row_params = row[self.PARAMETERS]
this_row_outputs = row[self.OUTPUTS]

for param in self.alg.parameterDefinitions():
if param.isDestination():
continue
if param.name() in this_row_params:
column = self.parameter_to_column[param.name()]
value = this_row_params[param.name()]
wrapper = self.wrappers[row_number][column]
wrapper.setParameterValue(value, context)

for out in self.alg.destinationParameterDefinitions():
if out.flags() & QgsProcessingParameterDefinition.Flag.FlagHidden:
continue
if out.name() in this_row_outputs:
column = self.parameter_to_column[out.name()]
value = this_row_outputs[out.name()].strip("'")
widget = self.tblParameters.cellWidget(row_number + 1, column)
widget.setValue(value)

def load_old_json_batch_file(self, values: List):
"""
Loads the old, insecure batch parameter JSON format
"""
message_box = QMessageBox()
message_box.setWindowTitle(self.tr("Security warning"))
message_box.setText(
self.tr("Opening older QGIS batch Processing files from an untrusted source can harm your computer. Only continue if you trust the source of the file. Continue?"))
message_box.setIcon(QMessageBox.Icon.Warning)
message_box.addButton(QMessageBox.StandardButton.Yes)
message_box.addButton(QMessageBox.StandardButton.No)
message_box.setDefaultButton(QMessageBox.StandardButton.No)
message_box.exec()
if message_box.result() != QMessageBox.StandardButton.Yes:
return

self.clear()
context = dataobjects.createContext()
try:
for row, alg in enumerate(values):
self.addRow()
Expand All @@ -585,15 +649,15 @@ def load(self):
except TypeError:
QMessageBox.critical(
self,
self.tr('Error'),
self.tr('An error occurred while reading your file.'))
self.tr('Load Batch Parameters'),
self.tr('An error occurred while reading the batch parameters file.'))

def save(self):
toSave = []
row_parameters = []
context = dataobjects.createContext()
for row in range(self.batchRowCount()):
algParams = {}
algOutputs = {}
this_row_params = {}
this_row_outputs = {}
alg = self.alg
for param in alg.parameterDefinitions():
if param.isDestination():
Expand All @@ -609,7 +673,7 @@ def save(self):
param.description(), row + 2)
self.parent.messageBar().pushMessage("", msg, level=Qgis.MessageLevel.Warning, duration=5)
return
algParams[param.name()] = param.valueAsPythonString(value, context)
this_row_params[param.name()] = param.valueAsJsonObject(value, context)

for out in alg.destinationParameterDefinitions():
if out.flags() & QgsProcessingParameterDefinition.Flag.FlagHidden:
Expand All @@ -618,28 +682,34 @@ def save(self):
widget = self.tblParameters.cellWidget(row + 1, col)
text = widget.getValue()
if text.strip() != '':
algOutputs[out.name()] = text.strip()
this_row_outputs[out.name()] = text.strip()
else:
self.parent.messageBar().pushMessage("",
self.tr('Wrong or missing output value: {0} (row {1})').format(
out.description(), row + 2),
level=Qgis.MessageLevel.Warning, duration=5)
return
toSave.append({self.PARAMETERS: algParams, self.OUTPUTS: algOutputs})
row_parameters.append({self.PARAMETERS: this_row_params, self.OUTPUTS: this_row_outputs})

output_json = {
self.FORMAT: self.CURRENT_FORMAT,
self.ROWS: row_parameters
}

settings = QgsSettings()
last_path = settings.value("/Processing/LastBatchPath", QDir.homePath())
filename, __ = QFileDialog.getSaveFileName(self,
self.tr('Save Batch'),
last_path,
self.tr('JSON files (*.json)'))
if filename:
if not filename.endswith('.json'):
filename += '.json'
last_path = QFileInfo(filename).path()
settings.setValue('/Processing/LastBatchPath', last_path)
with open(filename, 'w') as f:
json.dump(toSave, f)
self.tr('Batch Processing files (*.batch)'))
if not filename:
return

filename = QgsFileUtils.ensureFileNameHasExtension(filename, ['batch'])
last_path = QFileInfo(filename).path()
settings.setValue('/Processing/LastBatchPath', last_path)
with open(filename, 'w') as f:
json.dump(output_json, f, indent=2)

def setCellWrapper(self, row, column, wrapper, context):
self.wrappers[row - 1][column] = wrapper
Expand Down

0 comments on commit 6c1de7f

Please sign in to comment.