fix: stop Doctor exposing hash fragments as unset variables (#1558)

Classify Compose stderr unset-variable warnings into intentional references vs literal-dollar fragments from secret values. Adds env-literal-dollar preflight rule and safe remediation text. Fixes #1550.
This commit is contained in:
Anso
2026-07-05 03:50:40 -04:00
committed by GitHub
parent ecd757270f
commit 122c1b8073
12 changed files with 416 additions and 25 deletions
+28 -1
View File
@@ -82,11 +82,37 @@ const envUnset: PreflightRule = {
title: `Unset variable ${name}`,
message: `"${name}" is referenced by the Compose model but is not set in the environment or any consulted env file. Compose substitutes an empty string, which often breaks the container silently.`,
sourcePath: name,
remediation: `Define ${name} in a .env or env_file, or give it a default with \${${name}:-value}.`,
remediation: `Define ${name} in a .env or env_file, or give it a default with \${${name}:-value}. If the value is a literal secret or hash containing \`$\`, escape \`$\` as \`$$\` in Compose YAML or single-quote the value in an env file.`,
}));
},
};
const LITERAL_DOLLAR_REMEDIATION =
'If this was intended as a Compose variable, define it in the project environment or give it a default. '
+ 'If it is part of a literal secret or hash, escape literal dollar signs as `$$` in Compose YAML or single-quote the value in an env file.';
const envLiteralDollar: PreflightRule = {
id: 'env-literal-dollar',
run(ctx) {
return ctx.literalDollarWarnings.map(w => {
const likelySecret = w.likelySecret;
const title = likelySecret
? 'Literal dollar sign in likely secret value may be interpolated'
: 'Literal dollar sign in environment value may be interpolated';
const keyHint = w.envKey ? ` for "${w.envKey}"` : '';
return {
ruleId: 'env-literal-dollar',
severity: 'high' as const,
title,
message: `Compose treated a literal $ sequence inside an environment value${keyHint} as variable interpolation and may substitute an empty string for part of the value.`,
sourcePath: w.envKey,
remediation: LITERAL_DOLLAR_REMEDIATION,
service: w.service,
};
});
},
};
const envFileMissing: PreflightRule = {
id: 'env-file-missing',
run(ctx) {
@@ -730,6 +756,7 @@ const sensitiveServiceBroadExposure: PreflightRule = {
export const PREFLIGHT_RULES: PreflightRule[] = [
renderFailed,
envUnset,
envLiteralDollar,
envFileMissing,
portConflictNode,
portConflictInternal,
+4 -1
View File
@@ -1,5 +1,6 @@
import type { EffectiveModel } from './effectiveModel';
import type { ExposureIntent } from '../network/types';
import type { LiteralDollarWarning } from '../../helpers/unsetEnvClassification';
/** Graded severity of a single preflight finding. */
export type PreflightSeverity = 'blocker' | 'high' | 'warning' | 'info';
@@ -87,8 +88,10 @@ export interface PreflightContext {
renderable: boolean;
/** Redacted + truncated render error, or null. */
renderError: string | null;
/** Variable names Compose reported as unset (defaulted to empty string). */
/** Variable names Compose reported as unset (intentional references only). */
unsetEnvVars: string[];
/** Literal `$` sequences misread as variables; never includes fragment names. */
literalDollarWarnings: LiteralDollarWarning[];
/** Declared `env_file:` paths that are required but absent on disk (names only). */
missingEnvFiles: MissingEnvFile[];
/** Service names parsed from the literal source file (pre-render). */