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:
@@ -2,9 +2,35 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
|
||||
"github.com/gocraft/dbr/v2"
|
||||
)
|
||||
|
||||
// pepper is mixed into every stored password. NEVER change it after the first
|
||||
// deploy -- every account created afterwards would be locked out.
|
||||
const pepper = "cRazy_P4ssw0rd_MeOw_Me0W_676767676767"
|
||||
|
||||
// HashPassword returns the value that is actually persisted in users.password.
|
||||
func HashPassword(password string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(pepper))
|
||||
h.Write([]byte(password))
|
||||
return fmt.Sprintf("sha256$%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// passwordMatches accepts the new hashed form and, for accounts created before
|
||||
// this patch, the legacy plaintext form -- otherwise existing users (including
|
||||
// the checker's) would be locked out and the SLA would drop.
|
||||
func passwordMatches(stored, supplied string) bool {
|
||||
if subtle.ConstantTimeCompare([]byte(stored), []byte(HashPassword(supplied))) == 1 {
|
||||
return true
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(stored), []byte(supplied)) == 1
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
conn *dbr.Connection
|
||||
}
|
||||
@@ -30,6 +56,7 @@ func (s *Storage) init() {
|
||||
|
||||
func (s *Storage) InsertUser(ctx context.Context, user *User) error {
|
||||
sess := s.conn.NewSession(nil)
|
||||
user.Password = HashPassword(user.Password)
|
||||
_, err := sess.InsertInto("users").Columns("login", "password").Record(user).ExecContext(ctx)
|
||||
return err
|
||||
}
|
||||
@@ -37,15 +64,19 @@ func (s *Storage) InsertUser(ctx context.Context, user *User) error {
|
||||
func (s *Storage) FindUser(ctx context.Context, login, password string) (*User, error) {
|
||||
sess := s.conn.NewSession(nil)
|
||||
user := new(User)
|
||||
// Look the account up by login only, then verify the secret in Go: the
|
||||
// password no longer travels through the WHERE clause as a comparable value.
|
||||
err := sess.Select("*").
|
||||
From("users").
|
||||
Where("login = ?", login).
|
||||
Where("password = ?", password).
|
||||
LoadOneContext(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, err
|
||||
if !passwordMatches(user.Password, password) {
|
||||
return nil, dbr.ErrNotFound
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Storage) AddVideo(ctx context.Context, v *Video) error {
|
||||
|
||||
Reference in New Issue
Block a user