tiktak: LFI/auth-bypass в /vtt/, утечка превью, креды
- handleVtt: id валидируется как положительное целое, ошибка GetVideo
больше не игнорируется. Закрывает сразу два вектора: обход ACL через
нулевой db.Video (Private=false => haveAccess пропускал анонима) и
path traversal в path.Join(VttFolder, vid+".vtt").
- handleCreate: резкое превью приватного видео удаляется после блюра,
иначе оно оставалось доступным в public/static через echo.Static.
- main.go: DSN приведён к паролю из compose (5935004 поменял только
compose, из-за чего сервис не достучался бы до базы).
- db: пароли хешируются sha256 с pepper, сверка в Go constant-time,
плейнтекст принимается как legacy => старые юзеры не теряют доступ.
- cookies: HttpOnly + Path=/ + SameSite=Lax.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,12 @@ func (s *Server) handleCreate(c echo.Context) error {
|
||||
if err = video.Blur(ppath, s.privatePreviewPath(v.ID)); err != nil {
|
||||
return c.Render(http.StatusUnprocessableEntity, "create", ErrorResponse{"failed to blur preview: " + err.Error()})
|
||||
}
|
||||
// public/static is served by echo.Static, so the sharp preview of a
|
||||
// private video would otherwise be downloadable by anyone at
|
||||
// /public/static/preview_<id>.png -- drop it once it has been blurred.
|
||||
if err = os.Remove(ppath); err != nil && !os.IsNotExist(err) {
|
||||
return c.Render(http.StatusUnprocessableEntity, "create", ErrorResponse{"failed to drop preview: " + err.Error()})
|
||||
}
|
||||
}
|
||||
return c.Redirect(http.StatusFound, fmt.Sprintf("/watch/%d", v.ID))
|
||||
}
|
||||
@@ -180,14 +186,28 @@ func (s *Server) handleWatch(c echo.Context) error {
|
||||
|
||||
func (s *Server) handleVtt(c echo.Context) error {
|
||||
ac := c.(*auth.Context)
|
||||
vid := c.QueryParam("id")
|
||||
uid := ac.GetId()
|
||||
|
||||
// The id must be a plain positive integer. Anything else (./7, x/../7,
|
||||
// ../../etc/foo) is rejected before it can reach either the DB or the FS.
|
||||
vid, err := strconv.ParseInt(c.QueryParam("id"), 10, 64)
|
||||
if err != nil || vid <= 0 {
|
||||
return c.NoContent(http.StatusNotFound)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
v, _ := s.db.GetVideo(ctx, vid)
|
||||
if !s.haveAccess(ctx, uid, *v) {
|
||||
v, err := s.db.GetVideo(ctx, vid)
|
||||
// The error MUST be handled: a missing row leaves a zero db.Video
|
||||
// (UserID=0, Private=false) which haveAccess() would happily accept.
|
||||
if err != nil || v == nil {
|
||||
return c.NoContent(http.StatusNotFound)
|
||||
}
|
||||
|
||||
if !s.haveAccess(ctx, ac.GetId(), *v) {
|
||||
return c.NoContent(http.StatusForbidden)
|
||||
}
|
||||
vttPath := path.Join(s.c.VttFolder, vid+".vtt")
|
||||
|
||||
// Build the path from the validated integer, never from raw user input.
|
||||
vttPath := path.Join(s.c.VttFolder, strconv.FormatInt(vid, 10)+".vtt")
|
||||
return c.File(vttPath)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user