Compare commits

...

1 Commits

Author SHA1 Message Date
detail-app[bot] 0a3255d20d fix(templates): generate database credentials from a CSPRNG 2026-09-04 02:52:49 +00:00
2 changed files with 69 additions and 4 deletions
@@ -0,0 +1,67 @@
import { randomInt } from "node:crypto";
import { generatePassword } from "@dokploy/server/templates";
import { describe, expect, it, vi } from "vitest";
// Wrap `node:crypto.randomInt` with a spy that delegates to the real CSPRNG
// so we can confirm `generatePassword` draws from it while keeping true
// CSPRNG output for every behavioral assertion. `vi.mock` is hoisted above
// the `import` statements automatically.
vi.mock("node:crypto", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:crypto")>();
return {
...actual,
randomInt: vi.fn(actual.randomInt),
};
});
describe("generatePassword", () => {
it("defaults to a 16-character string", () => {
expect(generatePassword()).toHaveLength(16);
});
it.each([6, 16, 32])(
"generates a string of length %d when requested",
(length) => {
expect(generatePassword(length)).toHaveLength(length);
},
);
// `.toLowerCase()` is load-bearing: the function is dual-use and the
// app/service-name suffix callers (buildAppName, createPreviewDeployment)
// require DNS-label / Docker-Swarm-service-name-safe lowercase output.
it("produces only lowercase alphanumeric characters", () => {
for (let i = 0; i < 200; i++) {
expect(generatePassword()).toMatch(/^[a-z0-9]+$/);
}
});
// Credential material must come from a CSPRNG, not Math.random().
// This guards against reintroducing the regression fixed here.
it("does not source randomness from Math.random()", () => {
const randomSpy = vi.spyOn(Math, "random");
randomSpy.mockClear();
generatePassword(32);
expect(randomSpy).not.toHaveBeenCalled();
randomSpy.mockRestore();
});
it("sources randomness from node:crypto (crypto.randomInt)", () => {
const randomIntSpy = randomInt as unknown as ReturnType<typeof vi.fn>;
randomIntSpy.mockClear();
const password = generatePassword(16);
expect(randomIntSpy).toHaveBeenCalled();
expect(password).toHaveLength(16);
});
it("produces distinct values across calls", () => {
const samples = new Set<string>();
for (let i = 0; i < 1000; i++) {
samples.add(generatePassword());
}
expect(samples.size).toBe(1000);
});
});
+2 -4
View File
@@ -1,4 +1,4 @@
import { createHmac, randomBytes } from "node:crypto";
import { createHmac, randomBytes, randomInt } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -61,9 +61,7 @@ export const generatePassword = (quantity = 16): string => {
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let password = "";
for (let i = 0; i < quantity; i++) {
password += characters.charAt(
Math.floor(Math.random() * characters.length),
);
password += characters.charAt(randomInt(0, characters.length));
}
return password.toLowerCase();
};