Files
ALPHA-TRAIN2/services/tiktak/db/db.go
T
bobiqqandClaude Opus 5 fac56abb1d 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>
2026-08-26 11:42:05 +03:00

141 lines
3.9 KiB
Go

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
}
func New(conn *dbr.Connection) *Storage {
s := Storage{conn}
s.init()
return &s
}
func (s *Storage) init() {
for _, q := range []string{
"CREATE TABLE IF NOT EXISTS users(id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, login VARCHAR(200) UNIQUE , password VARCHAR(200))",
"CREATE TABLE IF NOT EXISTS video(id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, user_id INTEGER, description VARCHAR(500), private BOOLEAN, link VARCHAR(400))",
"CREATE TABLE IF NOT EXISTS access(id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, user_id INTEGER, video_id INTEGER)",
} {
_, err := s.conn.Exec(q)
if err != nil {
panic(err)
}
}
}
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
}
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).
LoadOneContext(ctx, user)
if err != nil {
return nil, err
}
if !passwordMatches(user.Password, password) {
return nil, dbr.ErrNotFound
}
return user, nil
}
func (s *Storage) AddVideo(ctx context.Context, v *Video) error {
sess := s.conn.NewSession(nil)
_, err := sess.InsertInto("video").
Columns("user_id", "description", "private", "link").
Record(v).ExecContext(ctx)
return err
}
func (s *Storage) ListVideo(ctx context.Context, limit int) (v []Video, err error) {
sess := s.conn.NewSession(nil)
_, err = sess.Select("*").
From("video").
OrderDesc("id").
Limit(uint64(limit)).
LoadContext(ctx, &v)
return
}
func (s *Storage) ListUserVideo(ctx context.Context, userId int64) (v []Video, err error) {
sess := s.conn.NewSession(nil)
_, err = sess.Select("*").
From("video").
Where("user_id = ?", userId).
OrderDesc("id").LoadContext(ctx, &v)
return
}
func (s *Storage) GetVideo(ctx context.Context, id interface{}) (*Video, error) {
sess := s.conn.NewSession(nil)
v := new(Video)
err := sess.Select("*").From("video").Where("id = ?", id).LoadOneContext(ctx, v)
return v, err
}
func (s *Storage) HaveAccess(ctx context.Context, id interface{}, userId int64) bool {
sess := s.conn.NewSession(nil)
var c int
err := sess.Select("count(*)").
From("access").
Where("user_id = ?", userId).
Where("video_id = ?", id).
LoadOneContext(ctx, &c)
if err != nil {
return false
}
return c > 0
}
func (s *Storage) AddAccess(ctx context.Context, id int, userId int64) error {
sess := s.conn.NewSession(nil)
_, err := sess.InsertInto("access").
Pair("video_id", id).
Pair("user_id", userId).
ExecContext(ctx)
return err
}