Fix PoW auth bypass: verify against issued challenge, not new one

Store challenge from handleGetChallenge in session, then verify
against it in handleSolve. Previously a fresh challenge was
generated for verification, making auth trivially bypassable.
This commit is contained in:
Niko Marmeladkov 2026-06-30 13:47:17 +03:00
parent dbb2782dd7
commit 4f6310c2bb
Signed by untrusted user who does not match committer: Niko
GPG key ID: E3B955F9442D44E3
2 changed files with 22 additions and 2 deletions

View file

@ -233,6 +233,7 @@ func (s *Server) handleStream(ctx context.Context, stream *quic.Stream, sess *Se
func (s *Server) handleGetChallenge(sess *Session) {
challenge := auth.NewChallenge(s.cfg.PoW.Difficulty)
sess.SetChallenge(challenge)
if st := sess.Stream(); st != nil {
st.WriteFrame(byte(types.OpChallenge), challenge.Marshal())
}
@ -245,7 +246,12 @@ func (s *Server) handleSolve(sess *Session, payload []byte) {
return
}
challenge := auth.NewChallenge(s.cfg.PoW.Difficulty)
challenge := sess.GetChallenge()
if challenge == nil {
sendOpError(sess.Stream(), types.ErrPoWInvalid, "no challenge issued")
return
}
if err := challenge.Verify(solution, s.cfg.PoW.ChallengeTTL); err != nil {
sendOpError(sess.Stream(), types.ErrPoWInvalid, err.Error())
return

View file

@ -4,6 +4,7 @@ import (
"sync"
"github.com/apernet/quic-go"
"github.com/niko/qcc/internal/auth"
"github.com/niko/qcc/internal/protocol"
"github.com/niko/qcc/internal/transport"
)
@ -19,6 +20,7 @@ type Session struct {
certDER []byte
activeCallID uint64
lastChallenge *auth.PoWChallenge
}
func NewSession(conn *quic.Conn) *Session {
@ -90,3 +92,15 @@ func (s *Session) ClearCall() {
defer s.mu.Unlock()
s.activeCallID = 0
}
func (s *Session) SetChallenge(c *auth.PoWChallenge) {
s.mu.Lock()
defer s.mu.Unlock()
s.lastChallenge = c
}
func (s *Session) GetChallenge() *auth.PoWChallenge {
s.mu.Lock()
defer s.mu.Unlock()
return s.lastChallenge
}