Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions internal/routes/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package routes

import (
"EverythingSuckz/fsb/config"
"EverythingSuckz/fsb/internal/utils"
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"

"github.com/gin-gonic/gin"
)

func (e *allRoutes) LoadGenerate(r *Route) {
genLog := e.log.Named("Generate")
defer genLog.Info("Loaded generate route")
r.Engine.GET("/generate/:messageID", getGenerateRoute)
}

// writeJSON writes a JSON response without HTML-escaping special chars like &.
func writeJSON(ctx *gin.Context, status int, v any) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.Encode(v)
ctx.Data(status, "application/json; charset=utf-8", buf.Bytes())
}

func getGenerateRoute(ctx *gin.Context) {
messageIDParam := ctx.Param("messageID")
messageID, err := strconv.Atoi(messageIDParam)
if err != nil {
writeJSON(ctx, http.StatusBadRequest, gin.H{"ok": false, "error": "invalid message ID"})
return
}

token := ctx.Query("token")
if token == "" {
token = ctx.GetHeader("X-Bot-Token")
}
if token == "" || token != config.ValueOf.BotToken {
writeJSON(ctx, http.StatusUnauthorized, gin.H{"ok": false, "error": "unauthorized"})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not accept bot token via query parameter.

Allowing token in URL leaks credentials through logs, browser history, and referrers. Require header-only auth for this endpoint.

πŸ›‘οΈ Proposed fix
-	token := ctx.Query("token")
-	if token == "" {
-		token = ctx.GetHeader("X-Bot-Token")
-	}
+	token := ctx.GetHeader("X-Bot-Token")
 	if token == "" || token != config.ValueOf.BotToken {
 		writeJSON(ctx, http.StatusUnauthorized, gin.H{"ok": false, "error": "unauthorized"})
 		return
 	}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
token := ctx.Query("token")
if token == "" {
token = ctx.GetHeader("X-Bot-Token")
}
if token == "" || token != config.ValueOf.BotToken {
writeJSON(ctx, http.StatusUnauthorized, gin.H{"ok": false, "error": "unauthorized"})
token := ctx.GetHeader("X-Bot-Token")
if token == "" || token != config.ValueOf.BotToken {
writeJSON(ctx, http.StatusUnauthorized, gin.H{"ok": false, "error": "unauthorized"})
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/routes/generate.go` around lines 39 - 44, The handler currently
accepts a token from URL query via ctx.Query("token") which risks credential
leakage; update the auth logic in this handler to only read the token from the
header (ctx.GetHeader("X-Bot-Token")) and remove the ctx.Query("token") check
and any fallback behavior so that comparison against config.ValueOf.BotToken is
performed only against the header value; ensure the unauthorized response
(writeJSON(..., http.StatusUnauthorized, ...)) remains intact when the header is
missing or does not match.

return
}

expParam := ctx.Query("exp")
var expiresAt int64
var expiryLabel string

if expParam == "0" {
expiresAt = 0
expiryLabel = "never"
} else {
var expiryDuration time.Duration
if expParam != "" {
d, err := time.ParseDuration(expParam)
if err != nil {
writeJSON(ctx, http.StatusBadRequest, gin.H{"ok": false, "error": fmt.Sprintf("invalid exp duration: %s", expParam)})
return
}
expiryDuration = d
} else {
expiryDuration = 24 * time.Hour
}
expiresAt = time.Now().Add(expiryDuration).Unix()
expiryLabel = expiryDuration.String()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

sig := utils.SignURL(messageID, expiresAt)
link := fmt.Sprintf("%s/stream/%d?exp=%s&sig=%s",
config.ValueOf.Host,
messageID,
strconv.FormatInt(expiresAt, 10),
sig,
)

if ctx.Query("redirect") == "true" {
ctx.Redirect(http.StatusFound, link)
return
}

writeJSON(ctx, http.StatusOK, gin.H{
"ok": true,
"url": link,
"expires_at": expiresAt,
"expires_in": expiryLabel,
})
}
40 changes: 29 additions & 11 deletions internal/routes/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,26 @@ func getStreamRoute(ctx *gin.Context) {
}

authHash := ctx.Query("hash")
if authHash == "" {
http.Error(w, "missing hash param", http.StatusBadRequest)
expParam := ctx.Query("exp")

if expParam == "" && authHash == "" {
http.Error(w, "missing auth: provide hash or exp+sig", http.StatusBadRequest)
return
}

// If exp is present, verify signature early before loading file
if expParam != "" {
sig := ctx.Query("sig")
if sig == "" {
http.Error(w, "missing sig param", http.StatusBadRequest)
return
}
if reason, ok := utils.VerifyURL(sig, messageID, expParam); !ok {
http.Error(w, reason, http.StatusForbidden)
return
}
}

worker := bot.GetNextWorker()

file, err := utils.TimeFuncWithResult(log, "FileFromMessage", func() (*types.File, error) {
Expand All @@ -52,15 +67,18 @@ func getStreamRoute(ctx *gin.Context) {
return
}

expectedHash := utils.PackFile(
file.FileName,
file.FileSize,
file.MimeType,
file.ID,
)
if !utils.CheckHash(authHash, expectedHash) {
http.Error(w, "invalid hash", http.StatusBadRequest)
return
// If no exp, validate via hash
if expParam == "" {
expectedHash := utils.PackFile(
file.FileName,
file.FileSize,
file.MimeType,
file.ID,
)
if !utils.CheckHash(authHash, expectedHash) {
http.Error(w, "invalid hash", http.StatusBadRequest)
return
}
}

// for photo messages
Expand Down
29 changes: 29 additions & 0 deletions internal/utils/hashing.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ package utils
import (
"EverythingSuckz/fsb/config"
"EverythingSuckz/fsb/internal/types"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"time"
)

func PackFile(fileName string, fileSize int64, mimeType string, fileID int64) string {
Expand All @@ -16,3 +22,26 @@ func GetShortHash(fullHash string) string {
func CheckHash(inputHash string, expectedHash string) bool {
return inputHash == GetShortHash(expectedHash)
}

// SignURL generates a truncated HMAC-SHA256 signature for messageID:expiry.
func SignURL(messageID int, expiry int64) string {
payload := fmt.Sprintf("%d:%d", messageID, expiry)
mac := hmac.New(sha256.New, []byte(config.ValueOf.BotToken))
mac.Write([]byte(payload))
return hex.EncodeToString(mac.Sum(nil))[:config.ValueOf.HashLength]
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func VerifyURL(sig string, messageID int, expiryStr string) (string, bool) {
expiry, err := strconv.ParseInt(expiryStr, 10, 64)
if err != nil {
return "invalid expiry", false
}
if expiry != 0 && time.Now().Unix() > expiry {
return "link has expired", false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
expected := SignURL(messageID, expiry)
if !hmac.Equal([]byte(sig), []byte(expected)) {
return "invalid signature", false
}
return "", true
}