Skip to content
Merged
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
63 changes: 62 additions & 1 deletion lib/api/routes/jobRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,60 @@ import { getSettings } from '../../services/storage/settingsStorage.js';

const DEMO_JOB_NAME = 'Demo-Job';

/**
* Notification adapter fields that touch the filesystem or other sensitive
* process-level resources. Non-admin users must not be able to introduce or
* modify these fields on a job, otherwise any authenticated user could make
* the server open/create files at arbitrary locations writable by the process
* (e.g. the sqlite adapter's `dbPath`).
*/
const PRIVILEGED_ADAPTER_FIELDS = {
sqlite: ['dbPath'],
};

function getAdapterFields(adapterList, adapterId) {
if (!Array.isArray(adapterList)) return {};
const entry = adapterList.find((a) => a && a.id === adapterId);
return (entry && entry.fields) || {};
}

/**
* Ensures a non-admin user cannot introduce or change privileged notification
* adapter fields (e.g. sqlite `dbPath`). Returns either:
* { ok: true, notificationAdapter } — safe to persist
* { ok: false, adapterId, field } — non-admin attempted to set or
* change a privileged field
*
* Admins are unaffected, so existing configurations continue to work unchanged.
* Non-admin requests that omit privileged fields (or echo back the admin-set
* value unchanged) are also allowed through.
*/
function sanitiseNotificationAdapter(notificationAdapter, existingJob, requestIsAdmin) {
if (requestIsAdmin || !Array.isArray(notificationAdapter)) {
return { ok: true, notificationAdapter };
}

for (const adapter of notificationAdapter) {
if (!adapter || typeof adapter !== 'object') continue;
const privilegedFields = PRIVILEGED_ADAPTER_FIELDS[adapter.id];
if (!privilegedFields || !privilegedFields.length) continue;

const incomingFields = adapter.fields || {};
const existingFields = existingJob ? getAdapterFields(existingJob.notificationAdapter, adapter.id) : {};
for (const key of privilegedFields) {
if (!Object.prototype.hasOwnProperty.call(incomingFields, key)) continue;
const incomingValue = incomingFields[key];
const existingValue = existingFields[key];
// Allow the no-op case where the client echoes back the admin-set value
// (common when editing a shared job through the UI).
if (incomingValue === existingValue) continue;
return { ok: false, adapterId: adapter.id, field: key };
}
}

return { ok: true, notificationAdapter };
}

function doesJobBelongsToUser(job, request) {
const userId = request.session.currentUser;
if (userId == null) return false;
Expand Down Expand Up @@ -171,14 +225,21 @@ export default async function jobPlugin(fastify) {
return reply.code(403).send({ error: 'Sorry, but you cannot change the Status of our Demo Job ;)' });
}

const sanitised = sanitiseNotificationAdapter(notificationAdapter, jobFromDb, isAdmin(request));
if (!sanitised.ok) {
return reply.code(403).send({
error: `You are not allowed to set the "${sanitised.field}" field on the "${sanitised.adapterId}" notification adapter. Please ask an administrator to configure this value.`,
});
}

jobStorage.upsertJob({
userId: request.session.currentUser,
jobId,
enabled,
name,
blacklist,
provider,
notificationAdapter,
notificationAdapter: sanitised.notificationAdapter,
shareWithUsers,
spatialFilter,
specFilter,
Expand Down
Loading