VNX-1047: Modules with Circular Dependencies
Detects Python and JavaScript/TypeScript modules of the same project that import each other, directly or through a third module, including cycles hidden by an import deferred into a function.
| Field | Value |
|---|---|
| Rule ID | VNX-1047 |
| Severity | Low |
| SARIF level | note |
| Kind | sast |
| Languages | Python, JavaScript / TypeScript |
| CWE | CWE-1047 |
| CWSS vector | CWSS:1.0/TI:L/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:L/DI:H/EX:L/EC:N/P:L |
| Tags | circular-dependency import initialization code-quality cwe-1047 |
| Source | internal/sast/rules/vnx-cwe1047-auto.rego |
Overview
VNX-1047 resolves the imports of every Python and JavaScript/TypeScript file to files of the scanned tree and reports the modules that depend on each other in a cycle. This is CWE-1047: Modules with Circular Dependencies.
What is resolved:
- Python:
import a.b,from a.b import c(includingcas a submodule),from .x import y,from ..x import yandfrom . import name. An absolute import is resolved from every directory above the importer that is not itself a package, soimport jsoninside a package is the standard library, not a localjson/directory. - JavaScript/TypeScript:
require('./x'),import ... from './x',import './x',export ... from './x'andimport('./x'), with the usual extension andindexresolution, and./x.jsin TypeScript source naming./x.ts.
What is reported:
- Two modules that import each other when they are loaded: once, on the import in the file whose path sorts first.
- Three modules in a load-time cycle: once, on the import made by the module whose path sorts first.
- An import moved into a function body that closes a cycle (of any length) with load-time imports back to the same module: on the deferred import. Deferring the import is the usual workaround; the dependency cycle is still there.
What is not reported: imports under if TYPE_CHECKING: or if __name__ == "__main__":, import type and import lists made only of type names (on one line or across several), import text inside a multi-line template literal (a code generator’s output), cycles in which every import is deferred, package and standard-library imports, path aliases (@/x), commented-out imports, and load-time cycles longer than three modules. Go is not covered: the Go compiler rejects import cycles.
Why This Matters
When two modules import each other, whichever loads first sees the other only partially initialised. In Python that is an ImportError: cannot import name ... (most likely due to a circular import) or an attribute that is None at first use; in CommonJS it is an empty exports object; in ES modules it is a binding in its temporal dead zone. The objects affected are often exactly the ones that are set up once at import time: a logger, a validator, an authentication handler or a configuration object. A check that silently sees undefined is a check that does not run.
What Gets Flagged
# shop/billing.py
from shop.orders import Order, OrderStatus # FLAGGED: shop/orders.py imports shop.billing back
# shop/orders.py
from shop.billing import charge
# server/database.py
def init_db():
import models # FLAGGED: models.py imports database at load time
Base.metadata.create_all(bind=engine)
// lib/anti.ts
import { readFixes } from '../routes/fixes' // FLAGGED: routes/fixes.ts -> lib/challenges.ts -> lib/anti.ts
What Is Not Flagged
if TYPE_CHECKING:
from .server import Server # type-only, never executed
import type { Customer } from './customer'
import { type Address, type Region } from './customer'
Remediation
- Move the code both modules need into a third module that imports neither of them.
- Invert one dependency: pass the object in (a parameter, a registration call) instead of importing it.
- In TypeScript, import names used only as types with
import type. - Do not hide a cycle by moving the import into a function; it keeps the coupling and moves the failure to the first call.
References
- CWE-1047: Modules with Circular Dependencies
- Python: the import system
- Node.js: cycles in CommonJS modules
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-1047
// nosec vnx-1047
Or record an ignore rule: vulnetix ignore add --rule vnx-1047 --reason "...". See Suppressing findings.