#178

Closed-term lint can be bypassed via .constructor and decoy declarations

Openakramcodezopened 9d ago

Summary

lintClosedTerm (shared/compiler/lint.ts) is the only guard that keeps connector fns "closed terms", but it can be bypassed in two ways. A fn that passes the lint can reach the host's real globals (fetch, process, Deno, ...). engine/link.ts then instantiates it with new Function(...), which runs in the global scope, so nothing else stops it.

This matters because the auth inject fn receives the resolved credentials in params, and the repo accepts community connector PRs.

Bypass 1: .constructor reaches Function

The lint treats obj.prop names as non-references, and the whitelist exposes Object, Array, String, etc. So Function is reachable without any free identifier:

(data) => [].constructor.constructor("return globalThis")()
(data) => Object.constructor("return globalThis")()

Bypass 2: scope over-approximation

The lint treats any name declared anywhere inside the fn as bound everywhere inside it, so a decoy declaration hides a real global:

(data) => {
    const unused = () => { const fetch = 1; return fetch; };
    return typeof fetch; // the real global fetch; lint passes
}

Reproduction

Running the unmodified lint.ts over these sources (TypeScript AST only, run under Node) gives:

SourceLintWhen executed
Deno.env.get("X") (control)rejected, as intendedn/a
[].constructor.constructor("return typeof process")()passes"object"
Object.constructor("return typeof Deno")()passesruns
decoy const fetch + typeof fetchpasses"function"

I grepped shared/compiler, engine and shared/core for constructor, __proto__ and globalThis and found no other mitigation. CI runs only deno task check and deno task test.

Impact

A connector merged with such a fn can read credentials in auth.inject and send them out via fetch, or run arbitrary code wherever the compiled bundle is executed. It needs a merged connector, so it is a supply-chain problem rather than remotely exploitable. I don't know whether the hosted Relay sandboxes fns separately.

Activity

1
akramcodez9d ago

Hey @ooctoo777 @FeiyouG, I found a bypass in the closed-term lint (#178) that lets a fn reach real globals like fetch or process, and opened a fix in #183. All existing connectors still compile and the full test suite passes. Please take a look and let me know if you'd like any changes?