Skip to content

SYN011: warn on dynamic import() calls that bypass the module capability model #152

@marcelofarias

Description

@marcelofarias

Problem

The SYN bypass series covers globals that make real effects without declaring them in the fn header: `fetch()` (SYN007), `WebSocket` (SYN008), `XMLHttpRequest` (SYN009), `setTimeout/setInterval/queueMicrotask` (SYN010). Dynamic `import()` is the same class of bypass but for module loading.

`import(specifier)` at runtime:

  • Loads a module whose capabilities are not statically declared
  • Is invisible to CAP001 (which only checks stdlib namespace calls)
  • Can load code with arbitrary capabilities (`net`, `fs`, etc.) that the calling fn does not declare
  • Creates an implicit dependency on the entire capability surface of the dynamically loaded module
  • Cannot be checked by the compiler's transitive closure since the specifier may be determined at runtime

A fn that calls `import(specifier)` has an undeclared capability surface proportional to everything the dynamically loaded module might do. The manifest hash proves the fn body hasn't changed; it says nothing about what the dynamically loaded module does at runtime.

Proposed diagnostic

SYN011 fires when a fn body calls `import(...)` — the dynamic import form — at `?bs 0.7+`.

Detection: the token `import` followed by `(` (not preceded by `.`/`?.`). This excludes static `import { ... } from` and `import * as ns from` declarations which are top-level and resolved at compile time.

Cases to detect:

  • `import(specifier)` — bare dynamic import
  • `import(`./module`)` — template literal specifier
  • `const mod = await import(path)` — awaited dynamic import

Suppression: `unsafe "loads plugin dynamically" { import(specifier) }`

Relation to existing diagnostics

  • Same bypass class as SYN007/SYN008/SYN009: a global that acquires a real capability without declaring it
  • More severe: dynamic import loads arbitrary code; the capability surface is unbounded
  • CAP001 will not fire even if the dynamically loaded module uses `http.`, `fs.`, etc.

Draft rewrite

```
// before — undeclared module load
async fn loadPlugin(name: string) uses { fs } -> Plugin {
const mod = await import(`./plugins/${name}`)
return mod.default
}

// after — explicit escape hatch
async fn loadPlugin(name: string) uses { fs } -> Plugin {
const mod = await unsafe "loads plugin by name from trusted plugin dir" { import(`./plugins/${name}`) }
return mod.default
}
```

Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    compilerCompiler / transform passenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions