fix(script): reject async/generator JS transforms and catch throwing getters

This commit is contained in:
aykhans committed 2026-09-30 19:53:37 +04:00
1 parent e85a6d4613
commit bcf2ea3446
2 files changed
+73 -23

No files matched your search

+72 -23
View File
@@ -2,6 +2,7 @@ package script
import ( import (
"errors" "errors"
"reflect"
"strconv" "strconv"
"github.com/dop251/goja" "github.com/dop251/goja"
@@ -18,6 +19,9 @@ type JsEngine struct {
http httpBridge http httpBridge
} }
// promiseType is what a transform returns when it is declared async or as a generator.
var promiseType = reflect.TypeFor[*goja.Promise]()
// NewJsEngine creates a new JavaScript script engine with the given script content. // NewJsEngine creates a new JavaScript script engine with the given script content.
// The script must define a global `transform` function that takes a request object // The script must define a global `transform` function that takes a request object
// and returns the modified request object. // and returns the modified request object.
@@ -58,6 +62,12 @@ func NewJsEngine(scriptContent string) (*JsEngine, error) {
return nil, types.NewScriptExecutionError("JavaScript", errors.New("'transform' must be a function")) return nil, types.NewScriptExecutionError("JavaScript", errors.New("'transform' must be a function"))
} }
// A generator returns an iterator, which is an ordinary object to Go, so the
// call cannot tell it apart and the declaration is the only thing left to read.
if isGeneratorFunction(vm, transformVal) {
return nil, types.ErrScriptTransformAsync
}
engine.transform = transform engine.transform = transform
return engine, nil return engine, nil
@@ -84,6 +94,13 @@ func (e *JsEngine) Transform(req *RequestData) error {
return types.NewScriptExecutionError("JavaScript", e.exceptionError(err)) return types.NewScriptExecutionError("JavaScript", e.exceptionError(err))
} }
// An async or generator transform returns a promise nothing ever awaits, and a promise
// carries none of the request fields, so the request would go out untransformed.
// ExportType reads the type alone, while Export would run every getter on the result.
if result.ExportType() == promiseType {
return types.ErrScriptTransformAsync
}
// Update RequestData from the returned object // Update RequestData from the returned object
if err := e.objectToRequestData(result, req); err != nil { if err := e.objectToRequestData(result, req); err != nil {
return types.NewScriptExecutionError("JavaScript", err) return types.NewScriptExecutionError("JavaScript", err)
@@ -144,34 +161,40 @@ func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
return types.ErrScriptTransformReturnObject return types.ErrScriptTransformReturnObject
} }
// Method // Reading a field runs the script's own getters and toString, which throw as a Go
if v := obj.Get("method"); v != nil && !goja.IsUndefined(v) { // panic outside goja's execution context, so the whole read runs inside Try.
req.Method = v.String() if thrown := e.runtime.Try(func() {
} // Method
if v := obj.Get("method"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Method = v.String()
}
// Path // Path
if v := obj.Get("path"); v != nil && !goja.IsUndefined(v) { if v := obj.Get("path"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Path = v.String() req.Path = v.String()
} }
// Body // Body
if v := obj.Get("body"); v != nil && !goja.IsUndefined(v) { if v := obj.Get("body"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Body = v.String() req.Body = v.String()
} }
// Headers // Headers
if v := obj.Get("headers"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { if v := obj.Get("headers"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Headers = e.objectToStringSliceMap(v.ToObject(e.runtime)) req.Headers = e.objectToStringSliceMap(v.ToObject(e.runtime))
} }
// Params // Params
if v := obj.Get("params"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { if v := obj.Get("params"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Params = e.objectToStringSliceMap(v.ToObject(e.runtime)) req.Params = e.objectToStringSliceMap(v.ToObject(e.runtime))
} }
// Cookies // Cookies
if v := obj.Get("cookies"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) { if v := obj.Get("cookies"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Cookies = e.objectToStringSliceMap(v.ToObject(e.runtime)) req.Cookies = e.objectToStringSliceMap(v.ToObject(e.runtime))
}
}); thrown != nil {
return e.exceptionError(thrown)
} }
return nil return nil
@@ -245,3 +268,29 @@ func jsStringValue(v goja.Value) (string, bool) {
} }
return v.String(), true return v.String(), true
} }
// isGeneratorFunction reports whether a function is declared with a star.
// The name comes from the script's own prototype chain, so it is a hint, not a guarantee.
// Every read here can run a script getter, so the whole walk stays inside Try.
func isGeneratorFunction(runtime *goja.Runtime, value goja.Value) bool {
var name string
thrown := runtime.Try(func() {
obj := value.ToObject(runtime)
if obj == nil {
return
}
constructor := obj.Get("constructor")
if constructor == nil || goja.IsUndefined(constructor) || goja.IsNull(constructor) {
return
}
constructorObj := constructor.ToObject(runtime)
if constructorObj == nil {
return
}
if value := constructorObj.Get("name"); value != nil {
name = value.String()
}
})
return thrown == nil && name == "GeneratorFunction"
}
+1
View File
@@ -434,6 +434,7 @@ var (
ErrScriptSourceEmpty = errors.New("script source cannot be empty after @") ErrScriptSourceEmpty = errors.New("script source cannot be empty after @")
ErrScriptTransformMissing = errors.New("script must define a global 'transform' function") ErrScriptTransformMissing = errors.New("script must define a global 'transform' function")
ErrScriptTransformReturnObject = errors.New("transform function must return an object") ErrScriptTransformReturnObject = errors.New("transform function must return an object")
ErrScriptTransformAsync = errors.New("transform function must be synchronous, async and generator functions are not supported")
ErrScriptURLNoHost = errors.New("script URL must have a host") ErrScriptURLNoHost = errors.New("script URL must have a host")
ErrScriptHTTPOutsideTransform = errors.New("http functions can only be called while transform is running") ErrScriptHTTPOutsideTransform = errors.New("http functions can only be called while transform is running")
ErrScriptHTTPUnavailable = errors.New("http client is not available") ErrScriptHTTPUnavailable = errors.New("http client is not available")