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
5 changes: 5 additions & 0 deletions .changeset/fix-env-secret-format-parsing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@docker-doctor/cli": patch
---

Fix `no-secrets-in-env` rule to support space-separated `ENV KEY VALUE` format, ensuring hardcoded credentials in this format are correctly detected by the linter.
88 changes: 57 additions & 31 deletions packages/core/src/rules/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,38 +58,64 @@ export const noSecretsInEnv: DockerfileRule = {

for (const inst of instructions) {
if (inst.instruction === "ENV" || inst.instruction === "ARG") {
const parts = inst.args.split(/\s+/u);
// ENV can be KEY=VALUE or KEY VALUE
for (const part of parts) {
const eqIndex = part.indexOf("=");
let key = "";
let value = "";

if (eqIndex > 0) {
key = part.slice(0, eqIndex);
value = part.slice(eqIndex + 1);
} else {
// Might be ENV KEY VALUE
key = part;
const args = inst.args.trim();
if (inst.instruction === "ENV" && !args.includes("=")) {
// KEY VALUE format
const match = args.match(/^(?<key>[^\s]+)\s+(?<value>.*)$/u);
if (match?.groups) {
const { key, value } = match.groups;
const isSecretKey = secretKeywords.some((regex) => regex.test(key));
if (
isSecretKey &&
value &&
!value.startsWith("$") &&
!value.startsWith("{")
) {
diagnostics.push(
createDiagnostic(
file,
this.key,
this.defaultSeverity as "error" | "warning" | "info",
`Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`,
this.help,
inst.line
)
);
}
}

const isSecretKey = secretKeywords.some((regex) => regex.test(key));
if (
isSecretKey &&
value &&
!value.startsWith("$") &&
!value.startsWith("{")
) {
diagnostics.push(
createDiagnostic(
file,
this.key,
this.defaultSeverity as "error" | "warning" | "info",
`Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`,
this.help,
inst.line
)
);
} else {
// Existing KEY=VALUE logic
const parts = args.split(/\s+/u);
for (const part of parts) {
const eqIndex = part.indexOf("=");
let key = "";
let value = "";

if (eqIndex > 0) {
key = part.slice(0, eqIndex);
value = part.slice(eqIndex + 1);
} else {
key = part;
}

const isSecretKey = secretKeywords.some((regex) => regex.test(key));
if (
isSecretKey &&
value &&
!value.startsWith("$") &&
!value.startsWith("{")
) {
diagnostics.push(
createDiagnostic(
file,
this.key,
this.defaultSeverity as "error" | "warning" | "info",
`Potential secret found in ${inst.instruction}: '${key}'. Secrets baked into images can be extracted easily by anyone with image access.`,
this.help,
inst.line
)
);
}
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions packages/core/test/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ describe("Security Rules", () => {
`);
const diags = noSecretsInEnv.check(withSecret, "Dockerfile");
expect(diags).toHaveLength(1);

const withSpaceSecret = parseDockerfile(`
ENV DB_PASSWORD my-secret-pass
`);
const diagsSpace = noSecretsInEnv.check(withSpaceSecret, "Dockerfile");
expect(diagsSpace).toHaveLength(1);

const withSpaceNormal = parseDockerfile(`
ENV NORMAL_VAR my-value
`);
const diagsSpaceNormal = noSecretsInEnv.check(
withSpaceNormal,
"Dockerfile"
);
expect(diagsSpaceNormal).toHaveLength(0);
});
});

Expand Down
Loading