mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50: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>
114 lines
3.1 KiB
Go
114 lines
3.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/goccy/go-json"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/hashicorp/go-uuid"
|
|
|
|
"github.com/nezhahq/nezha/model"
|
|
"github.com/nezhahq/nezha/pkg/websocketx"
|
|
"github.com/nezhahq/nezha/proto"
|
|
"github.com/nezhahq/nezha/service/rpc"
|
|
"github.com/nezhahq/nezha/service/singleton"
|
|
)
|
|
|
|
// Create web ssh terminal
|
|
// @Summary Create web ssh terminal
|
|
// @Description Create web ssh terminal
|
|
// @Tags auth required
|
|
// @Accept json
|
|
// @Param terminal body model.TerminalForm true "TerminalForm"
|
|
// @Produce json
|
|
// @Success 200 {object} model.CreateTerminalResponse
|
|
// @Router /terminal [post]
|
|
func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) {
|
|
var createTerminalReq model.TerminalForm
|
|
if err := c.ShouldBind(&createTerminalReq); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
server, _ := singleton.ServerShared.Get(createTerminalReq.ServerID)
|
|
if server == nil || server.TaskStream == nil {
|
|
return nil, singleton.Localizer.ErrorT("server not found or not connected")
|
|
}
|
|
|
|
if !server.HasPermission(c) {
|
|
return nil, singleton.Localizer.ErrorT("permission denied")
|
|
}
|
|
|
|
streamId, err := uuid.GenerateUUID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c))
|
|
|
|
terminalData, _ := json.Marshal(&model.TerminalTask{
|
|
StreamID: streamId,
|
|
})
|
|
if err := server.TaskStream.Send(&proto.Task{
|
|
Type: model.TaskTypeTerminalGRPC,
|
|
Data: string(terminalData),
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &model.CreateTerminalResponse{
|
|
SessionID: streamId,
|
|
ServerID: server.ID,
|
|
ServerName: server.Name,
|
|
}, nil
|
|
}
|
|
|
|
// TerminalStream web ssh terminal stream
|
|
// @Summary Terminal stream
|
|
// @Description Terminal stream
|
|
// @Tags auth required
|
|
// @Param id path string true "Stream UUID"
|
|
// @Success 200 {object} model.CommonResponse[any]
|
|
// @Router /ws/terminal/{id} [get]
|
|
func terminalStream(c *gin.Context) (any, error) {
|
|
streamId := c.Param("id")
|
|
// GHSA-style fix: io_stream sessions must be reachable only by their creator
|
|
// (or an admin). Without this, any authenticated user who learns a stream
|
|
// UUID — via Referer leak, access logs, browser history — can hijack a live
|
|
// terminal and gain shell access to the target server.
|
|
if !rpc.NezhaHandlerSingleton.IsStreamAuthorizedForUser(streamId, getUid(c), callerIsAdmin(c)) {
|
|
return nil, singleton.Localizer.ErrorT("permission denied")
|
|
}
|
|
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
|
|
return nil, err
|
|
}
|
|
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
|
|
|
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
if err != nil {
|
|
return nil, newWsError("%v", err)
|
|
}
|
|
defer wsConn.Close()
|
|
conn := websocketx.NewConn(wsConn)
|
|
|
|
go func() {
|
|
// PING 保活
|
|
for {
|
|
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
|
return
|
|
}
|
|
time.Sleep(time.Second * 10)
|
|
}
|
|
}()
|
|
|
|
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
|
|
return nil, newWsError("%v", err)
|
|
}
|
|
|
|
if err = rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10); err != nil {
|
|
return nil, newWsError("%v", err)
|
|
}
|
|
|
|
return nil, newWsError("")
|
|
}
|