feat(jsmod): dynamic sandboxing for db access v1.3.7

This commit is contained in:
AI Engineer 2026-05-31 20:36:21 +08:00
parent 2c22ce193b
commit 615b2b5338

View File

@ -3,32 +3,70 @@ package db
import ( import (
"context" "context"
"errors" "errors"
"strings"
"apigo.cc/go/cast"
"apigo.cc/go/file"
"apigo.cc/go/jsmod" "apigo.cc/go/jsmod"
) )
func init() { func init() {
jsmod.Register("db", map[string]any{ jsmod.Register("db", map[string]any{
"get": func(ctx context.Context, name string) (*jsDB, error) { "get": func(ctx context.Context, name string) (*jsDB, error) {
isReadOnly := false
// 安全模式下的动态判定
if jsmod.IsSafeMode(ctx) {
if strings.HasPrefix(name, "sqlite://") {
// 1. SQLite 路径校验
path := strings.TrimPrefix(name, "sqlite://")
if idx := strings.Index(path, "?"); idx != -1 {
path = path[:idx]
}
// 如果路径不在沙箱内VerifyPathForSafeMode 会直接返回 error阻止连接
_, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil {
return nil, err
}
isReadOnly = false
} else {
// 2. 远程数据库前缀校验
var allowedDSNs []string
cast.Convert(&allowedDSNs, ctx.Value("AllowedDSNs"))
matched := false
for _, prefix := range allowedDSNs {
if strings.HasPrefix(name, prefix) {
matched = true
break
}
}
if !matched {
// 未命中白名单,降级为只读模式
isReadOnly = true
}
}
}
d := GetDB(name, nil) d := GetDB(name, nil)
if d.Error != nil { if d.Error != nil {
return nil, d.Error return nil, d.Error
} }
return &jsDB{db: d, ctx: ctx}, nil return &jsDB{db: d, ctx: ctx, isReadOnly: isReadOnly}, nil
}, },
}) })
} }
type jsDB struct { type jsDB struct {
db *DB db *DB
ctx context.Context ctx context.Context
isReadOnly bool
} }
var errSafeMode = errors.New("database write operation is restricted in safe mode") var errReadOnly = errors.New("database is in read-only mode for this context")
func (jd *jsDB) checkSafe() error { func (jd *jsDB) checkWrite() error {
if jsmod.IsSafeMode(jd.ctx) { if jd.isReadOnly {
return errSafeMode return errReadOnly
} }
return nil return nil
} }
@ -40,59 +78,59 @@ func (jd *jsDB) Query(query string, args ...any) *QueryResult {
// Write Operations // Write Operations
func (jd *jsDB) Exec(query string, args ...any) *ExecResult { func (jd *jsDB) Exec(query string, args ...any) *ExecResult {
if jd.checkSafe() != nil { if err := jd.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jd.db.Exec(query, args...) return jd.db.Exec(query, args...)
} }
func (jd *jsDB) Insert(table string, data any) *ExecResult { func (jd *jsDB) Insert(table string, data any) *ExecResult {
if jd.checkSafe() != nil { if err := jd.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jd.db.Insert(table, data) return jd.db.Insert(table, data)
} }
func (jd *jsDB) Update(table string, data any, conditions string, args ...any) *ExecResult { func (jd *jsDB) Update(table string, data any, conditions string, args ...any) *ExecResult {
if jd.checkSafe() != nil { if err := jd.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jd.db.Update(table, data, conditions, args...) return jd.db.Update(table, data, conditions, args...)
} }
func (jd *jsDB) Delete(table string, conditions string, args ...any) *ExecResult { func (jd *jsDB) Delete(table string, conditions string, args ...any) *ExecResult {
if jd.checkSafe() != nil { if err := jd.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jd.db.Delete(table, conditions, args...) return jd.db.Delete(table, conditions, args...)
} }
func (jd *jsDB) Replace(table string, data any) *ExecResult { func (jd *jsDB) Replace(table string, data any) *ExecResult {
if jd.checkSafe() != nil { if err := jd.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jd.db.Replace(table, data) return jd.db.Replace(table, data)
} }
// Transaction Support // Transaction Support
func (jd *jsDB) Begin() (*jsTx, error) { func (jd *jsDB) Begin() (*jsTx, error) {
// Note: We don't block Begin() itself, but destructive operations within Tx will be blocked
tx := jd.db.Begin() tx := jd.db.Begin()
if tx.Error != nil { if tx.Error != nil {
return nil, tx.Error return nil, tx.Error
} }
return &jsTx{tx: tx, ctx: jd.ctx}, nil return &jsTx{tx: tx, ctx: jd.ctx, isReadOnly: jd.isReadOnly}, nil
} }
// jsTx wraps *Tx for JS environment // jsTx wraps *Tx for JS environment
type jsTx struct { type jsTx struct {
tx *Tx tx *Tx
ctx context.Context ctx context.Context
isReadOnly bool
} }
func (jt *jsTx) checkSafe() error { func (jt *jsTx) checkWrite() error {
if jsmod.IsSafeMode(jt.ctx) { if jt.isReadOnly {
return errSafeMode return errReadOnly
} }
return nil return nil
} }
@ -102,41 +140,47 @@ func (jt *jsTx) Query(query string, args ...any) *QueryResult {
} }
func (jt *jsTx) Exec(query string, args ...any) *ExecResult { func (jt *jsTx) Exec(query string, args ...any) *ExecResult {
if jt.checkSafe() != nil { if err := jt.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jt.tx.Exec(query, args...) return jt.tx.Exec(query, args...)
} }
func (jt *jsTx) Insert(table string, data any) *ExecResult { func (jt *jsTx) Insert(table string, data any) *ExecResult {
if jt.checkSafe() != nil { if err := jt.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jt.tx.Insert(table, data) return jt.tx.Insert(table, data)
} }
func (jt *jsTx) Update(table string, data any, conditions string, args ...any) *ExecResult { func (jt *jsTx) Update(table string, data any, conditions string, args ...any) *ExecResult {
if jt.checkSafe() != nil { if err := jt.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jt.tx.Update(table, data, conditions, args...) return jt.tx.Update(table, data, conditions, args...)
} }
func (jt *jsTx) Delete(table string, conditions string, args ...any) *ExecResult { func (jt *jsTx) Delete(table string, conditions string, args ...any) *ExecResult {
if jt.checkSafe() != nil { if err := jt.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jt.tx.Delete(table, conditions, args...) return jt.tx.Delete(table, conditions, args...)
} }
func (jt *jsTx) Replace(table string, data any) *ExecResult { func (jt *jsTx) Replace(table string, data any) *ExecResult {
if jt.checkSafe() != nil { if err := jt.checkWrite(); err != nil {
return &ExecResult{Error: errSafeMode} return &ExecResult{Error: err}
} }
return jt.tx.Replace(table, data) return jt.tx.Replace(table, data)
} }
func (jt *jsTx) Commit() error { return jt.tx.Commit() } func (jt *jsTx) Commit() error {
if err := jt.checkWrite(); err != nil {
return err
}
return jt.tx.Commit()
}
func (jt *jsTx) Rollback() error { return jt.tx.Rollback() } func (jt *jsTx) Rollback() error { return jt.tx.Rollback() }
// Metadata // Metadata