From 615b2b53386bf6e62015234f03520110793e04d8 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Sun, 31 May 2026 20:36:21 +0800 Subject: [PATCH] feat(jsmod): dynamic sandboxing for db access v1.3.7 --- js_export.go | 114 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 35 deletions(-) diff --git a/js_export.go b/js_export.go index 56fe917..eab912d 100644 --- a/js_export.go +++ b/js_export.go @@ -3,32 +3,70 @@ package db import ( "context" "errors" + "strings" + "apigo.cc/go/cast" + "apigo.cc/go/file" "apigo.cc/go/jsmod" ) func init() { jsmod.Register("db", map[string]any{ "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) if d.Error != nil { return nil, d.Error } - return &jsDB{db: d, ctx: ctx}, nil + return &jsDB{db: d, ctx: ctx, isReadOnly: isReadOnly}, nil }, }) } type jsDB struct { - db *DB - ctx context.Context + db *DB + 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 { - if jsmod.IsSafeMode(jd.ctx) { - return errSafeMode +func (jd *jsDB) checkWrite() error { + if jd.isReadOnly { + return errReadOnly } return nil } @@ -40,59 +78,59 @@ func (jd *jsDB) Query(query string, args ...any) *QueryResult { // Write Operations func (jd *jsDB) Exec(query string, args ...any) *ExecResult { - if jd.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jd.checkWrite(); err != nil { + return &ExecResult{Error: err} } return jd.db.Exec(query, args...) } func (jd *jsDB) Insert(table string, data any) *ExecResult { - if jd.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jd.checkWrite(); err != nil { + return &ExecResult{Error: err} } return jd.db.Insert(table, data) } func (jd *jsDB) Update(table string, data any, conditions string, args ...any) *ExecResult { - if jd.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jd.checkWrite(); err != nil { + return &ExecResult{Error: err} } return jd.db.Update(table, data, conditions, args...) } func (jd *jsDB) Delete(table string, conditions string, args ...any) *ExecResult { - if jd.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jd.checkWrite(); err != nil { + return &ExecResult{Error: err} } return jd.db.Delete(table, conditions, args...) } func (jd *jsDB) Replace(table string, data any) *ExecResult { - if jd.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jd.checkWrite(); err != nil { + return &ExecResult{Error: err} } return jd.db.Replace(table, data) } // Transaction Support 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() if tx.Error != nil { 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 type jsTx struct { - tx *Tx - ctx context.Context + tx *Tx + ctx context.Context + isReadOnly bool } -func (jt *jsTx) checkSafe() error { - if jsmod.IsSafeMode(jt.ctx) { - return errSafeMode +func (jt *jsTx) checkWrite() error { + if jt.isReadOnly { + return errReadOnly } return nil } @@ -102,41 +140,47 @@ func (jt *jsTx) Query(query string, args ...any) *QueryResult { } func (jt *jsTx) Exec(query string, args ...any) *ExecResult { - if jt.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jt.checkWrite(); err != nil { + return &ExecResult{Error: err} } return jt.tx.Exec(query, args...) } func (jt *jsTx) Insert(table string, data any) *ExecResult { - if jt.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jt.checkWrite(); err != nil { + return &ExecResult{Error: err} } return jt.tx.Insert(table, data) } func (jt *jsTx) Update(table string, data any, conditions string, args ...any) *ExecResult { - if jt.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jt.checkWrite(); err != nil { + return &ExecResult{Error: err} } return jt.tx.Update(table, data, conditions, args...) } func (jt *jsTx) Delete(table string, conditions string, args ...any) *ExecResult { - if jt.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jt.checkWrite(); err != nil { + return &ExecResult{Error: err} } return jt.tx.Delete(table, conditions, args...) } func (jt *jsTx) Replace(table string, data any) *ExecResult { - if jt.checkSafe() != nil { - return &ExecResult{Error: errSafeMode} + if err := jt.checkWrite(); err != nil { + return &ExecResult{Error: err} } 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() } // Metadata