mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
createTerminal and createFM correctly check server ownership before issuing a stream UUID, but terminalStream and fmStream only verified that the UUID existed. Any authenticated user holding a valid stream UUID could attach to it, gaining the original creator's live shell or file-manager session — and the UUID is exposed via URL path (referer leaks, access logs, browser history, frontend error reporters). Bind the creator user ID into ioStreamContext at CreateStream time, expose StreamOwnership and IsStreamAuthorizedForUser, and check ownership in terminalStream/fmStream before the WebSocket upgrade so a rejected attempt does not tear down the legitimate stream via defer. NAT streams are also routed through CreateStream(_, 0); they are not reachable from /ws/terminal or /ws/file so a sentinel user ID is fine. Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
198 lines
4.3 KiB
Go
198 lines
4.3 KiB
Go
package rpc
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/nezhahq/nezha/service/singleton"
|
|
)
|
|
|
|
type ioStreamContext struct {
|
|
creatorUserID uint64
|
|
userIo io.ReadWriteCloser
|
|
agentIo io.ReadWriteCloser
|
|
userIoConnectCh chan struct{}
|
|
agentIoConnectCh chan struct{}
|
|
userIoChOnce sync.Once
|
|
agentIoChOnce sync.Once
|
|
}
|
|
|
|
type bp struct {
|
|
buf []byte
|
|
}
|
|
|
|
var bufPool = sync.Pool{
|
|
New: func() any {
|
|
return &bp{
|
|
buf: make([]byte, 1024*1024),
|
|
}
|
|
},
|
|
}
|
|
|
|
func (s *NezhaHandler) CreateStream(streamId string, creatorUserID uint64) {
|
|
s.ioStreamMutex.Lock()
|
|
defer s.ioStreamMutex.Unlock()
|
|
|
|
s.ioStreams[streamId] = &ioStreamContext{
|
|
creatorUserID: creatorUserID,
|
|
userIoConnectCh: make(chan struct{}),
|
|
agentIoConnectCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// IsStreamAuthorizedForUser checks whether the requesting user may attach to
|
|
// the stream. A stream is reachable only by its creator or by an admin; any
|
|
// other authenticated user must be rejected. Unknown streams are always
|
|
// rejected.
|
|
func (s *NezhaHandler) IsStreamAuthorizedForUser(streamId string, userID uint64, isAdmin bool) bool {
|
|
creator, found := s.StreamOwnership(streamId)
|
|
if !found {
|
|
return false
|
|
}
|
|
if isAdmin {
|
|
return true
|
|
}
|
|
return creator == userID
|
|
}
|
|
|
|
// StreamOwnership returns the user ID that created the stream and whether the
|
|
// stream is still tracked. Callers must compare the returned creator against
|
|
// the requesting user before attaching to the stream — without this the
|
|
// channel becomes a session-hijack primitive (terminal/file manager RCE).
|
|
func (s *NezhaHandler) StreamOwnership(streamId string) (uint64, bool) {
|
|
s.ioStreamMutex.RLock()
|
|
defer s.ioStreamMutex.RUnlock()
|
|
|
|
ctx, ok := s.ioStreams[streamId]
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return ctx.creatorUserID, true
|
|
}
|
|
|
|
func (s *NezhaHandler) GetStream(streamId string) (*ioStreamContext, error) {
|
|
s.ioStreamMutex.RLock()
|
|
defer s.ioStreamMutex.RUnlock()
|
|
|
|
if ctx, ok := s.ioStreams[streamId]; ok {
|
|
return ctx, nil
|
|
}
|
|
|
|
return nil, errors.New("stream not found")
|
|
}
|
|
|
|
func (s *NezhaHandler) CloseStream(streamId string) error {
|
|
s.ioStreamMutex.Lock()
|
|
defer s.ioStreamMutex.Unlock()
|
|
|
|
if ctx, ok := s.ioStreams[streamId]; ok {
|
|
if ctx.userIo != nil {
|
|
ctx.userIo.Close()
|
|
}
|
|
if ctx.agentIo != nil {
|
|
ctx.agentIo.Close()
|
|
}
|
|
delete(s.ioStreams, streamId)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *NezhaHandler) UserConnected(streamId string, userIo io.ReadWriteCloser) error {
|
|
stream, err := s.GetStream(streamId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stream.userIo = userIo
|
|
stream.userIoChOnce.Do(func() {
|
|
close(stream.userIoConnectCh)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *NezhaHandler) AgentConnected(streamId string, agentIo io.ReadWriteCloser) error {
|
|
stream, err := s.GetStream(streamId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stream.agentIo = agentIo
|
|
stream.agentIoChOnce.Do(func() {
|
|
close(stream.agentIoConnectCh)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *NezhaHandler) StartStream(streamId string, timeout time.Duration) error {
|
|
stream, err := s.GetStream(streamId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
timeoutTimer := time.NewTimer(timeout)
|
|
|
|
LOOP:
|
|
for {
|
|
select {
|
|
case <-stream.userIoConnectCh:
|
|
if stream.agentIo != nil {
|
|
timeoutTimer.Stop()
|
|
break LOOP
|
|
}
|
|
case <-stream.agentIoConnectCh:
|
|
if stream.userIo != nil {
|
|
timeoutTimer.Stop()
|
|
break LOOP
|
|
}
|
|
case <-time.After(timeout):
|
|
break LOOP
|
|
}
|
|
time.Sleep(time.Millisecond * 500)
|
|
}
|
|
|
|
if stream.userIo == nil && stream.agentIo == nil {
|
|
return singleton.Localizer.ErrorT("timeout: no connection established")
|
|
}
|
|
if stream.userIo == nil {
|
|
return singleton.Localizer.ErrorT("timeout: user connection not established")
|
|
}
|
|
if stream.agentIo == nil {
|
|
return singleton.Localizer.ErrorT("timeout: agent connection not established")
|
|
}
|
|
|
|
isDone := new(atomic.Bool)
|
|
endCh := make(chan struct{})
|
|
|
|
go func() {
|
|
bp := bufPool.Get().(*bp)
|
|
defer bufPool.Put(bp)
|
|
_, innerErr := io.CopyBuffer(stream.userIo, stream.agentIo, bp.buf)
|
|
if innerErr != nil {
|
|
err = innerErr
|
|
}
|
|
if isDone.CompareAndSwap(false, true) {
|
|
close(endCh)
|
|
}
|
|
}()
|
|
go func() {
|
|
bp := bufPool.Get().(*bp)
|
|
defer bufPool.Put(bp)
|
|
_, innerErr := io.CopyBuffer(stream.agentIo, stream.userIo, bp.buf)
|
|
if innerErr != nil {
|
|
err = innerErr
|
|
}
|
|
if isDone.CompareAndSwap(false, true) {
|
|
close(endCh)
|
|
}
|
|
}()
|
|
|
|
<-endCh
|
|
return err
|
|
}
|