Skip to content

Fix AVP decode DoS, missing Origin-State-Id on DWA and MSCC dictionary bound - #259

Open
kulcsartibor wants to merge 1 commit into
fiorix:mainfrom
kulcsartibor:main
Open

Fix AVP decode DoS, missing Origin-State-Id on DWA and MSCC dictionary bound#259
kulcsartibor wants to merge 1 commit into
fiorix:mainfrom
kulcsartibor:main

Conversation

@kulcsartibor

Copy link
Copy Markdown
Contributor

Three independent fixes, each with a regression test that fails before the
change. They are unrelated to each other, so happy to split into separate PRs
if you'd prefer.

1. diam: remote-DoS panic on malformed AVPs in decodeAVPs

decodeAVPs treats a DecodeError as non-fatal and keeps going, but it
appended the partially-decoded AVP and then called a.Len() on it. For an AVP
whose payload failed to decode, Data is nil, so Len() dereferences nil and
panics. Any peer that can reach the endpoint can crash it with a crafted
message. A zero or garbage AVP length could also leave the loop without forward
progress.

Changes:

  • decodeAVPs now advances by the validated on-wire AVP length (padded to the
    next 4-byte boundary) on the DecodeError path instead of calling a.Len(),
    and breaks out on a non-advancing or out-of-range length so a malformed
    length field cannot spin the loop.
  • (*AVP).Len() falls back to the header length when Data == nil, as defense
    in depth for any other caller iterating a partially-decoded message.

This is complementary to #249, which fixed the same panic class in
DecodeFromBytes (type/length mismatch fallback) and DecodeGroupedFromBytes
(nil Data in the sub-AVP loop). Neither covers the top-level decodeAVPs
loop, which is what this addresses.

Tests: TestReadMessageMalformed (table of malformed buffers, incl. the
original crash input, an AVP with a too-short payload, and a zero-length AVP)
and FuzzReadMessage, asserting ReadMessage never panics on arbitrary input.

2. sm: send Origin-State-Id on DWA, not back into the request

In handleDWR, Origin-State-Id was appended to the received request m
instead of the answer a being built — the two lines immediately above it
correctly use a:

a.NewAVP(avp.OriginHost, avp.Mbit, 0, sm.cfg.OriginHost)
a.NewAVP(avp.OriginRealm, avp.Mbit, 0, sm.cfg.OriginRealm)
if sm.cfg.OriginStateID != 0 {
        stateid := datatype.Unsigned32(sm.cfg.OriginStateID)
        m.NewAVP(avp.OriginStateID, avp.Mbit, 0, stateid)  // -> a.NewAVP
}

Two consequences: the DWA never carried Origin-State-Id despite the code's
clear intent, and the inbound request object was mutated (its Message-Length
grew), which also corrupts the message passed to sm.Error's ErrorReport if
the answer fails to write.

I checked every other m.NewAVP in diam/sm: they all build requests, where
m is correct. This was the only occurrence in handler code.

Test: TestHandleDWR_OriginStateID completes the handshake, sends a DWR and
asserts the DWA carries Origin-State-Id matching the configured value. The
existing DWR tests missed this because they only assert the DWA's Result-Code.

3. dict: Multiple-Services-Credit-Control is unbounded in CCR/CCA

The Credit-Control command rules declared
<rule avp="Multiple-Services-Credit-Control" required="false" max="1"/>.
RFC 4006 lists the AVP as *[ Multiple-Services-Credit-Control ] in both CCR
and CCA, and the Ro rule in tgpp_ro_rf.xml is already unbounded — so the
dictionary contradicted both the RFC and its own sibling for the same message
structure.

Corrected in diam/dict/testdata/credit_control.xml and in the embedded
duplicate inside diam/dict/default.go (the two are kept in sync by hand;
editing only the XML would leave the shipped dictionary unchanged).

This is behaviourally inert: Rule.Max is only read by
printCommand/printAVP for the human-readable dictionary dump and is never
enforced on decode or encode, so nothing that parsed before parses differently
now. The only observable change is the dump, which prints max=0 for these
rules.

Test: TestCreditControlMSCCUnbounded.

Verification

go build ./...
go test ./...
go test ./diam -run '^$' -fuzz FuzzReadMessage -fuzztime 30s

All green on linux and darwin/arm64, except the pre-existing
TestServerClose/sctp failure on darwin. The 30 s fuzz run
completed ~2.3M executions with no crashes.

8 files changed, 236 insertions(+), 8 deletions(-).

…y bound

Three independent fixes.

diam: remote-DoS panic on malformed AVPs in decodeAVPs

  On a DecodeError, decodeAVPs appended the partially-decoded AVP and
  called a.Len() on it, dereferencing a nil Data and panicking — any peer
  able to reach the endpoint could crash it with a crafted message. A
  zero or garbage AVP length could also leave the loop without forward
  progress. decodeAVPs now advances by the validated on-wire length on
  the error path and stops on non-advancing lengths; (*AVP).Len() guards
  against nil Data as defense in depth. Adds TestReadMessageMalformed and
  FuzzReadMessage. Complementary to the panic fixes in fiorix#249, which cover
  DecodeFromBytes and DecodeGroupedFromBytes rather than the top-level
  loop.

sm: send Origin-State-Id on DWA, not back into the request

  handleDWR appended Origin-State-Id to the received request (m) instead
  of the answer (a) it was building — the two lines above it correctly
  use a. The DWA therefore never carried Origin-State-Id despite the
  code's intent, and the inbound request object was mutated (its
  Message-Length grew), which also corrupts the message handed to
  sm.Error's ErrorReport on a write failure. This is the only occurrence
  of the pattern in handler code; every other m.NewAVP in diam/sm builds
  a request, where m is correct. Adds TestHandleDWR_OriginStateID; the
  existing tests missed it because they only assert the DWA Result-Code.

dict: Multiple-Services-Credit-Control is unbounded in CCR/CCA

  The Credit-Control command rules declared the AVP with max="1", while
  RFC 4006 lists it as *[ Multiple-Services-Credit-Control ] in both CCR
  and CCA and the Ro rule in tgpp_ro_rf.xml is already unbounded — the
  dictionary contradicted both the RFC and its own sibling for the same
  message structure. Corrected in testdata/credit_control.xml and in the
  embedded duplicate in dict/default.go, which is kept in sync by hand.
  Behaviourally inert: Rule.Max is only read for the human-readable
  dictionary dump and is never enforced on decode or encode, so nothing
  that parsed before parses differently now. Adds
  TestCreditControlMSCCUnbounded.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant