CVE-2022-24450 (Go)

NATS JetStream ordered consumers can stall when flow-control accounting does not request a new flow-control marker after a pending marker has already been created and subsequent queued messages exceed the outstanding flow-control window. Under this condition, a consumer may stop delivering all pending messages even though they were successfully published, leaving the ordered consumer with fewer pending messages than expected and causing delivery to stall.

Weakness:
CWE-862: name: Missing Authorization
description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.

Repository: https://github.com/nats-io/nats-server

Vulnerable code:
file_path: server/consumer.go
snippet: func (o *consumer) needFlowControl() bool {
	if o.maxpb == 0 {
		return false
	}
	// Decide whether to send a flow control message which we will need the user to respond.
	// We send when we are over 50% of our current window limit.
	if o.fcid == _EMPTY_ && o.pbytes > o.maxpb/2 {
		return true
	}
	return false
}

Fixed code:
file_path: server/consumer.go
snippet: func (o *consumer) needFlowControl(sz int) bool {
	if o.maxpb == 0 {
		return false
	}
	// Decide whether to send a flow control message which we will need the user to respond.
	// We send when we are over 50% of our current window limit.
	if o.fcid == _EMPTY_ && o.pbytes > o.maxpb/2 {
		return true
	}
	// If we have an existing outstanding FC, check to see if we need to expand the o.fcsz
	if o.fcid != _EMPTY_ && (o.pbytes-o.fcsz) >= o.maxpb {
		o.fcsz += sz
	}
	return false
}

file_path: server/consumer.go
snippet: func (o *consumer) deliverMsg(dsubj, subj string, hdr, msg []byte, seq, dc uint64, ts int64) {
	if o.mset == nil {
		return
	}
	// Update pending on first attempt. This can go upside down for a short bit, that is ok.
	// See adjustedPending().
	if dc == 1 {
		o.sgap--
	}

	dseq := o.dseq
	o.dseq++

	// If headers only do not send msg payload.
	// Add in msg size itself as header.
	if o.cfg.HeadersOnly {
		var bb bytes.Buffer
		if len(hdr) == 0 {
			bb.WriteString(hdrLine)
		} else {
			bb.Write(hdr)
			bb.Truncate(len(hdr) - LEN_CR_LF)
		}
		bb.WriteString(JSMsgSize)
		bb.WriteString(": ")
		bb.WriteString(strconv.FormatInt(int64(len(msg)), 10))
		bb.WriteString(CR_LF)
		bb.WriteString(CR_LF)
		hdr = bb.Bytes()
		// Cancel msg payload
		msg = nil
	}

	pmsg := newJSPubMsg(dsubj, subj, o.ackReply(seq, dseq, dc, ts, o.adjustedPending()), hdr, msg, o, seq)
	psz := pmsg.size()

	if o.maxpb > 0 {
		o.pbytes += psz
	}

	mset := o.mset
	ap := o.cfg.AckPolicy

	// Send message.
	o.outq.send(pmsg)

	if ap == AckExplicit || ap == AckAll {
		o.trackPending(seq, dseq)
	} else if ap == AckNone {
		o.adflr = dseq
		o.asflr = seq
	}

	// Flow control.
	if o.maxpb > 0 && o.needFlowControl(psz) {
		o.sendFlowControl()
	}

	// FIXME(dlc) - Capture errors?
	o.updateDelivered(dseq, seq, dc, ts)

	// If we are ack none and mset is interest only we should make sure stream removes interest.
	if ap == AckNone && mset.cfg.Retention != LimitsPolicy {
		if o.node == nil || o.cfg.Direct {
			mset.ackq.push(seq)
		} else {
			o.updateAcks(dseq, seq)
		}
	}
}