From c50a60a66d40a4edf86758d11081d3bfc00d46c8 Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Thu, 1 Oct 2026 20:13:17 +0300 Subject: [PATCH] Fix: restrict field projection to inclusion shape on list-things and config reads Sanitizes the parsed fields projection to {key:1} on list-things, worker-config, global-config and global/data, blocking mingo field-path renames that leak device credentials and __proto__-path writes that pollute the process. --- workers/lib/server/handlers/global.handlers.js | 6 +++--- workers/lib/server/handlers/miners.handlers.js | 11 +---------- workers/lib/server/handlers/things.handlers.js | 6 +++--- workers/lib/utils.js | 14 ++++++++++++++ 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/workers/lib/server/handlers/global.handlers.js b/workers/lib/server/handlers/global.handlers.js index 626e2d41..3ca8fa63 100644 --- a/workers/lib/server/handlers/global.handlers.js +++ b/workers/lib/server/handlers/global.handlers.js @@ -1,7 +1,7 @@ 'use strict' const gLibUtilBase = require('@bitfinex/lib-js-util-base') const { GLOBAL_DATA_TYPES, LOCKED_TIMEZONE_DEFAULT, POOL_REBATE_SOURCES } = require('../../constants') -const { parseJsonQueryParam } = require('../../utils') +const { parseJsonQueryParam, sanitizeIncludeFields } = require('../../utils') const { getAutoPoolRebates, getCombinedPoolRebates, @@ -29,7 +29,7 @@ async function getGlobalData (ctx, req) { } if (req.query.fields) { - req.query.fields = parseJsonQueryParam(req.query.fields, 'ERR_FIELDS_INVALID_JSON') + req.query.fields = sanitizeIncludeFields(parseJsonQueryParam(req.query.fields, 'ERR_FIELDS_INVALID_JSON')) } if (type === GLOBAL_DATA_TYPES.POOL_REBATES) { @@ -146,7 +146,7 @@ async function setFeatures (ctx, req) { async function getGlobalConfig (ctx, req, rep) { if (req.query.fields) { - req.query.fields = parseJsonQueryParam(req.query.fields, 'ERR_FIELDS_INVALID_JSON') + req.query.fields = sanitizeIncludeFields(parseJsonQueryParam(req.query.fields, 'ERR_FIELDS_INVALID_JSON')) } return await ctx.dataProxy.requestDataMap('getGlobalConfig', req.query) diff --git a/workers/lib/server/handlers/miners.handlers.js b/workers/lib/server/handlers/miners.handlers.js index 577567f3..dd40c802 100644 --- a/workers/lib/server/handlers/miners.handlers.js +++ b/workers/lib/server/handlers/miners.handlers.js @@ -1,6 +1,6 @@ 'use strict' -const { parseJsonQueryParam, flattenRpcResults } = require('../../utils') +const { parseJsonQueryParam, flattenRpcResults, sanitizeIncludeFields } = require('../../utils') const { MINER_FIELD_MAP, MINER_PROJECTION_MAP, @@ -178,15 +178,6 @@ async function listMiners (ctx, req) { } } -function sanitizeIncludeFields (fields) { - if (!fields || typeof fields !== 'object') return null - const clean = {} - for (const [key, value] of Object.entries(fields)) { - if (value === 1) clean[key] = 1 - } - return Object.keys(clean).length ? clean : null -} - function summarizeMinerActivity (miners) { const summary = { total: miners.length, offline: 0, powerModes: {} } for (const miner of miners) { diff --git a/workers/lib/server/handlers/things.handlers.js b/workers/lib/server/handlers/things.handlers.js index 6b6a8c75..c7485a4f 100644 --- a/workers/lib/server/handlers/things.handlers.js +++ b/workers/lib/server/handlers/things.handlers.js @@ -4,7 +4,7 @@ const { AUTH_LEVELS, COMMENT_ACTION } = require('../../constants') -const { parseJsonQueryParam, flattenRpcResults } = require('../../utils') +const { parseJsonQueryParam, flattenRpcResults, sanitizeIncludeFields } = require('../../utils') const { assertSafeMongoQuery } = require('../lib/queryUtils') async function listThingsRoute (ctx, req, rep) { @@ -18,7 +18,7 @@ async function listThingsRoute (ctx, req, rep) { } if (req.query.fields) { - req.query.fields = parseJsonQueryParam(req.query.fields, 'ERR_FIELDS_INVALID_JSON') + req.query.fields = sanitizeIncludeFields(parseJsonQueryParam(req.query.fields, 'ERR_FIELDS_INVALID_JSON')) } return await ctx.dataProxy.requestDataMap('listThings', req.query) @@ -100,7 +100,7 @@ async function processThingComment (ctx, req, operation = COMMENT_ACTION.ADD) { async function getWorkerConfig (ctx, req, rep) { if (req.query.fields) { - req.query.fields = parseJsonQueryParam(req.query.fields, 'ERR_FIELDS_INVALID_JSON') + req.query.fields = sanitizeIncludeFields(parseJsonQueryParam(req.query.fields, 'ERR_FIELDS_INVALID_JSON')) } return await ctx.dataProxy.requestDataMap('getWrkConf', req.query) diff --git a/workers/lib/utils.js b/workers/lib/utils.js index 1e458292..6c31bf9d 100644 --- a/workers/lib/utils.js +++ b/workers/lib/utils.js @@ -73,6 +73,19 @@ const parseJsonQueryParam = (jsonString, errorCode = 'ERR_INVALID_JSON') => { } } +// Restricts a parsed `fields` projection to the `{ key: 1 }` inclusion shape the +// worker's openrpc contract declares. Dropping every other value blocks mingo +// field-path renames (`"$opts.password"`) that move credentials past the rack's +// post-projection redaction, and `__proto__`-path writes that pollute the process. +const sanitizeIncludeFields = (fields) => { + if (!fields || typeof fields !== 'object') return null + const clean = {} + for (const [key, value] of Object.entries(fields)) { + if (value === 1) clean[key] = 1 + } + return Object.keys(clean).length ? clean : null +} + const runParallel = (tasks) => new Promise((resolve, reject) => { async.parallel(tasks, (err, results) => { @@ -238,6 +251,7 @@ module.exports = { getRpcTimeout, getAuthTokenFromHeaders, parseJsonQueryParam, + sanitizeIncludeFields, getStartOfDay, localDayStart, localWeekStart,