From 5fef427ebb54eb0b2b48691488edaea25a97c6a7 Mon Sep 17 00:00:00 2001 From: Powersource Date: Wed, 13 Mar 2024 13:04:45 +0100 Subject: [PATCH] Validate all incoming payloads in Stream (#6) --- lib/range.js | 14 +++++++- lib/stream.js | 60 +++++++++++++++++++++++++++++--- lib/util.js | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 4 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 lib/util.js diff --git a/lib/range.js b/lib/range.js index 2a6bc7d..a854b94 100644 --- a/lib/range.js +++ b/lib/range.js @@ -1,3 +1,14 @@ +/** + * @param {any} range + * @return {range is Range} + */ +function isRange(range) { + if (!Array.isArray(range)) return false + if (range.length !== 2) return false + if (!Number.isInteger(range[0]) || !Number.isInteger(range[1])) return false + return true +} + /** * @typedef {[number, number]} Range */ @@ -26,7 +37,8 @@ function estimateMsgCount(range) { const EMPTY_RANGE = /** @type {Range} */ ([1, 0]) module.exports = { + isRange, isEmptyRange, estimateMsgCount, - EMPTY_RANGE + EMPTY_RANGE, } diff --git a/lib/stream.js b/lib/stream.js index 774e555..1ba1b07 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -1,6 +1,7 @@ // @ts-ignore const Pipeable = require('push-stream/pipeable') -const { isEmptyRange } = require('./range') +const { isRange, isEmptyRange } = require('./range') +const { isMsgId, isBloom, isMsgIds, isMsgs } = require('./util') /** * @typedef {ReturnType} PPPPPGoals @@ -449,18 +450,34 @@ class SyncStream extends Pipeable { * @param {Data} data */ write(data) { + if (!data) return this.#debug('Invalid data from remote peer: missing data') + // prettier-ignore + if (typeof data !== 'object') return this.#debug('Invalid data from remote peer: not an object') + // prettier-ignore + if (Array.isArray(data)) return this.#debug('Invalid data from remote peer: is an array') const { id, phase, payload } = data + // prettier-ignore + if (typeof phase !== 'number') return this.#debug("Invalid data from remote peer: phase isn't a number") + // prettier-ignore + if (!isMsgId(id)) return this.#debug('Invalid data from remote peer: id is not a valid msg id') + // prettier-ignore + if (phase !== 0 && !payload) return this.#debug('Invalid data from remote peer: payload is missing') - // TODO: validate that each data objects has the exact correct shape switch (phase) { case 0: { return this.#sendLocalHave(id) } case 1: { + // prettier-ignore + if (!isRange(payload)) return this.#debug('Invalid data from remote peer: payload is not a range in phase 1') + return this.#sendLocalHaveAndWant(id, payload) } case 2: { const { haveRange, wantRange } = payload + // prettier-ignore + if (!isRange(haveRange) || !isRange(wantRange)) return this.#debug('Invalid data from remote peer: haveRange or wantRange is not a range in phase 2') + if (isEmptyRange(haveRange)) { // prettier-ignore this.#debug('%s Stream IN2: received remote have-range %o and want-range %o for %s', this.#myId, haveRange, wantRange, id) @@ -471,6 +488,11 @@ class SyncStream extends Pipeable { } case 3: { const { wantRange, bloom } = payload + // prettier-ignore + if (!isRange(wantRange)) return this.#debug('Invalid data from remote peer: wantRange is not a range in phase 3') + // prettier-ignore + if (!isBloom(bloom)) return this.#debug('Invalid data from remote peer: bloom is not a bloom in phase 3') + const haveRange = this.#remoteHave.get(id) if (haveRange && isEmptyRange(haveRange)) { // prettier-ignore @@ -482,32 +504,62 @@ class SyncStream extends Pipeable { } case 4: { const { bloom, msgIDs } = payload + // prettier-ignore + if (!isBloom(bloom)) return this.#debug('Invalid data from remote peer: bloom is not a bloom in phase 4') + // prettier-ignore + if (!isMsgIds(msgIDs)) return this.#debug('Invalid data from remote peer: msgIDs is not an array of msg ids in phase 4') + return this.#sendBloomReq(id, phase + 1, 1, bloom, msgIDs) } case 5: { const { bloom, msgIDs } = payload + // prettier-ignore + if (!isBloom(bloom)) return this.#debug('Invalid data from remote peer: bloom is not a bloom in phase 5') + // prettier-ignore + if (!isMsgIds(msgIDs)) return this.#debug('Invalid data from remote peer: msgIDs is not an array of msg ids in phase 5') + return this.#sendBloomRes(id, phase + 1, 1, bloom, msgIDs) } case 6: { const { bloom, msgIDs } = payload + // prettier-ignore + if (!isBloom(bloom)) return this.#debug('Invalid data from remote peer: bloom is not a bloom in phase 6') + // prettier-ignore + if (!isMsgIds(msgIDs)) return this.#debug('Invalid data from remote peer: msgIDs is not an array of msg ids in phase 6') + return this.#sendBloomReq(id, phase + 1, 2, bloom, msgIDs) } case 7: { const { bloom, msgIDs } = payload + // prettier-ignore + if (!isBloom(bloom)) return this.#debug('Invalid data from remote peer: bloom is not a bloom in phase 7') + // prettier-ignore + if (!isMsgIds(msgIDs)) return this.#debug('Invalid data from remote peer: msgIDs is not an array of msg ids in phase 7') + return this.#sendMissingMsgsReq(id, 2, bloom, msgIDs) } case 8: { const { bloom, msgs } = payload + // prettier-ignore + if (!isBloom(bloom)) return this.#debug('Invalid data from remote peer: bloom is not a bloom in phase 8') + // prettier-ignore + if (!isMsgs(msgs)) return this.#debug('Invalid data from remote peer: msgs is not an array of msgs in phase 8') + return this.#sendMissingMsgsRes(id, 2, bloom, msgs) } case 9: { + // prettier-ignore + if (!isMsgs(payload)) return this.#debug('Invalid data from remote peer: payload is not an array of msgs in phase 9') + // prettier-ignore this.#debug('%s Stream IN9: got %s msgs in %s', this.#myId, payload.length, id) return this.#consumeMissingMsgs(id, payload) } + default: { + // prettier-ignore + return this.#debug('Invalid data from remote peer: phase is an invalid number') + } } - - this.#debug('Stream IN: unknown %o', data) } /** diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..146edf5 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,94 @@ +const bs58 = require('bs58') +/** + * @typedef {import('./range').Range} Range + * @typedef {import('ppppp-db/msg-v4').Msg} Msg + */ + +/** + * @param {any} msgId + * @return {msgId is string} + */ +function isMsgId(msgId) { + try { + const d = bs58.decode(msgId) + return d.length === 32 + } catch { + return false + } +} + +/** + * @param {any} msgIds + * @return {msgIds is Array} + */ +function isMsgIds(msgIds) { + if (!Array.isArray(msgIds)) return false + return msgIds.every(isMsgId) +} + +/** + * @param {any} msgs + * @return {msgs is Array} + */ +function isMsgs(msgs) { + if (!Array.isArray(msgs)) return false + return msgs.every(isMsg) +} + +/** + * @param {any} bloom + * @return {bloom is string} + */ +function isBloom(bloom) { + // TODO: validate when blooming is stabilized + return !!bloom +} + +/** + * @param {any} msg + * @returns {msg is Msg} + */ +function isMsg(msg) { + if (!msg || typeof msg !== 'object') { + return false + } + if (!('data' in msg)) { + return false + } + if (!msg.metadata || typeof msg.metadata !== 'object') { + return false + } + if (!('dataHash' in msg.metadata)) { + return false + } + if (!('dataSize' in msg.metadata)) { + return false + } + if (!('account' in msg.metadata)) { + return false + } + if (!('accountTips' in msg.metadata)) { + return false + } + if (!('tangles' in msg.metadata)) { + return false + } + if (!('domain' in msg.metadata)) { + return false + } + if (msg.metadata.v !== 4) { + return false + } + if (typeof msg.sig !== 'string') { + return false + } + return true +} + +module.exports = { + isMsgId, + isMsgIds, + isMsgs, + isBloom, + isMsg, +} diff --git a/package.json b/package.json index 99d7a9e..fd35307 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "bloom-filters": "^3.0.0", + "bs58": "^5.0.0", "debug": "^4.3.4", "promisify-4loc": "^1.0.0", "pull-stream": "^3.7.0", @@ -35,7 +36,6 @@ "@types/debug": "^4.1.9", "@types/pull-stream": "3.6.3", "@types/node": "16.x", - "bs58": "^5.0.0", "c8": "7", "ppppp-caps": "github:staltz/ppppp-caps#93fa810b9a40b78aef4872d4c2a8412cccb52929", "ppppp-db": "github:staltz/ppppp-db#cf1532965ea1d16929ed2291a9b737a4ce74caac",