fix(rpc): bind io_stream sessions to creator to prevent terminal/fm hijack

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>
This commit is contained in:
naiba
2026-05-18 15:16:40 +00:00
co-authored by naiba/CloudCode
parent e7c2e453c0
commit 6661d6a7fc
6 changed files with 238 additions and 5 deletions
+8 -1
View File
@@ -44,7 +44,7 @@ func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) {
return nil, err
}
rpc.NezhaHandlerSingleton.CreateStream(streamId)
rpc.NezhaHandlerSingleton.CreateStream(streamId, getUid(c))
terminalData, _ := json.Marshal(&model.TerminalTask{
StreamID: streamId,
@@ -72,6 +72,13 @@ func createTerminal(c *gin.Context) (*model.CreateTerminalResponse, error) {
// @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
}