From ad9c6c8d535a56f842274babd781d9a38413b377 Mon Sep 17 00:00:00 2001 From: Martin Najemi Date: Fri, 24 Apr 2026 10:40:00 +0200 Subject: [PATCH] fix: Propagate taint through lambda-returned dynamic imports Risk: low --- CHANGELOG.md | 7 +++++++ VERSION | 2 +- internal/tsparse/tsparse.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12cb528..975d806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.19.2] - 2026-04-24 + +### Fixed +- Bare dynamic `import("pkg")` calls (e.g. `() => import("pkg")` passed to a loader, or `const mod = await import("pkg")` used opaquely without property access) are now recorded as side-effect imports, so taint on the target package propagates to the importing file. Previously only the three pattern forms (var+property-access, destructure, `.then` callback) produced an `Import` record, leaving bare calls invisible to taint propagation. + ## [0.19.1] - 2026-04-23 ### Fixed @@ -267,6 +272,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Multi-stage Docker build - Automated vendor upgrade workflow +[0.19.2]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.19.1...v0.19.2 +[0.19.1]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.19.0...v0.19.1 [0.19.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.18.1...v0.19.0 [0.18.1]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.18.0...v0.18.1 [0.18.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.17.1...v0.18.0 diff --git a/VERSION b/VERSION index 4559101..7f78682 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.19.1 \ No newline at end of file +0.19.2 \ No newline at end of file diff --git a/internal/tsparse/tsparse.go b/internal/tsparse/tsparse.go index febeed9..ad57c54 100644 --- a/internal/tsparse/tsparse.go +++ b/internal/tsparse/tsparse.go @@ -387,6 +387,10 @@ func extractDynamicImports(sf *ast.SourceFile, analysis *FileAnalysis) { // Phase 1: collect dynamic imports assigned to variables or destructured // varName → specifier (for pattern 1) varImports := make(map[string]string) + // Every dynamic import() specifier seen, regardless of surrounding context. + // Used to emit side-effect imports for calls no other pattern captures + // (e.g. `() => import("pkg")` passed to a loader). + allSpecifiers := make(map[string]bool) var walkPhase1 func(n *ast.Node) walkPhase1 = func(n *ast.Node) { @@ -394,6 +398,10 @@ func extractDynamicImports(sf *ast.SourceFile, analysis *FileAnalysis) { return } + if spec := extractDynamicImportSpecifier(n); spec != "" { + allSpecifiers[spec] = true + } + if n.Kind == ast.KindVariableDeclaration { vd := n.AsVariableDeclaration() name := vd.Name() @@ -438,6 +446,7 @@ func extractDynamicImports(sf *ast.SourceFile, analysis *FileAnalysis) { } if len(varImports) == 0 { + emitSideEffectDynamicImports(analysis, allSpecifiers) return } @@ -488,6 +497,28 @@ func extractDynamicImports(sf *ast.SourceFile, analysis *FileAnalysis) { Source: specifier, }) } + + emitSideEffectDynamicImports(analysis, allSpecifiers) +} + +// emitSideEffectDynamicImports adds a side-effect Import entry (empty Names) for +// every dynamic import specifier that none of the pattern-based phases captured. +// Covers bare calls like `() => import("pkg")` and `const mod = await import("pkg")` +// where `mod` is used opaquely (no property access). Downstream taint treats these +// as full-taint imports, matching how static `import "pkg"` is handled. +func emitSideEffectDynamicImports(analysis *FileAnalysis, allSpecifiers map[string]bool) { + if len(allSpecifiers) == 0 { + return + } + covered := make(map[string]bool) + for _, imp := range analysis.Imports { + covered[imp.Source] = true + } + for spec := range allSpecifiers { + if !covered[spec] { + analysis.Imports = append(analysis.Imports, Import{Source: spec}) + } + } } // extractDynamicImportSpecifier checks if an expression is (or contains)