Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/core/src/config/variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ export const substitute = Effect.fn("ConfigVariable.substitute")(function* (inpu
const separator = token.indexOf(":-")
const name = separator === -1 ? token : token.slice(0, separator)
const fallback = separator === -1 ? "" : token.slice(separator + 2)
const value = (input.env?.[name] ?? process.env[name]) || fallback
// Own-property resolution only: a plain lookup reads through the prototype
// chain, so {env:constructor} would inject the Object function source (and
// ignore any :- fallback) instead of resolving as unset.
const supplied = input.env && Object.hasOwn(input.env, name) ? input.env[name] : undefined
const value = (supplied ?? (Object.hasOwn(process.env, name) ? process.env[name] : undefined)) || fallback
push(escape ? JSON.stringify(value).slice(1, -1) : value, false)
cursor = match.index + match[0].length
}
Expand Down
20 changes: 20 additions & 0 deletions packages/core/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,26 @@ describe("Config", () => {
}).pipe(Effect.provide(AppNodeBuilder.build(FSUtil.node))),
)

it.effect("resolves env references by own property, not through the prototype chain", () =>
Effect.gen(function* () {
const substitute = (text: string, env?: Record<string, string>) =>
ConfigVariable.substitute({ type: "virtual", source: "test", dir: "/", text, ...(env ? { env } : {}) })
for (const name of ["constructor", "toString", "hasOwnProperty", "valueOf", "__proto__"]) {
const text = `{ "v": "{env:${name}}", "f": "{env:${name}:-FALLBACK}" }`
// Not actually set in the supplied env: resolves as unset ("" / the :- fallback),
// never as the inherited Object.prototype member.
expect(JSON.parse(yield* substitute(text, {}))).toEqual({ v: "", f: "FALLBACK" })
// Same on the process.env path (no env supplied, the normal config-file path).
expect(JSON.parse(yield* substitute(text))).toEqual({ v: "", f: "FALLBACK" })
// Genuinely set under a prototype name in the supplied env: substitutes its real value.
expect(JSON.parse(yield* substitute(text, Object.fromEntries([[name, "real"]])))).toEqual({
v: "real",
f: "real",
})
}
}).pipe(Effect.provide(AppNodeBuilder.build(FSUtil.node))),
)

it.effect("does not substitute env references on // comment lines", () =>
Effect.gen(function* () {
const substitute = (text: string, env: Record<string, string>) =>
Expand Down
Loading