Skip to content

Commit 8a5ef6b

Browse files
committed
Make our hash validation more strict. ALWAYS validate, even on cache hits.
1 parent 2c40423 commit 8a5ef6b

3 files changed

Lines changed: 94 additions & 9 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as sinon from "sinon";
2+
import { expect } from "chai";
3+
import * as fs from "fs-extra";
4+
import * as path from "path";
5+
import * as downloadUtils from "../downloadUtils";
6+
import { getOrDownloadUniversalMaker } from "./universalMakerDownload";
7+
8+
describe("universalMakerDownload", () => {
9+
let existsSyncStub: sinon.SinonStub;
10+
let ensureDirSyncStub: sinon.SinonStub;
11+
let copySyncStub: sinon.SinonStub;
12+
let chmodSyncStub: sinon.SinonStub;
13+
let downloadToTmpStub: sinon.SinonStub;
14+
let validateSizeStub: sinon.SinonStub;
15+
let validateChecksumStub: sinon.SinonStub;
16+
17+
let originalPlatform: string;
18+
let originalArch: string;
19+
20+
beforeEach(() => {
21+
originalPlatform = process.platform;
22+
originalArch = process.arch;
23+
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
24+
Object.defineProperty(process, "arch", { value: "x64", configurable: true });
25+
26+
existsSyncStub = sinon.stub(fs, "existsSync");
27+
ensureDirSyncStub = sinon.stub(fs, "ensureDirSync");
28+
copySyncStub = sinon.stub(fs, "copySync");
29+
chmodSyncStub = sinon.stub(fs, "chmodSync");
30+
downloadToTmpStub = sinon.stub(downloadUtils, "downloadToTmp");
31+
validateSizeStub = sinon.stub(downloadUtils, "validateSize");
32+
validateChecksumStub = sinon.stub(downloadUtils, "validateChecksum");
33+
});
34+
35+
afterEach(() => {
36+
sinon.restore();
37+
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
38+
Object.defineProperty(process, "arch", { value: originalArch, configurable: true });
39+
40+
});
41+
42+
it("should return cached binary if valid", async () => {
43+
existsSyncStub.returns(true);
44+
validateSizeStub.resolves();
45+
validateChecksumStub.resolves();
46+
47+
const result = await getOrDownloadUniversalMaker();
48+
expect(existsSyncStub).to.have.been.calledOnce;
49+
expect(validateSizeStub).to.have.been.calledOnce;
50+
expect(validateChecksumStub).to.have.been.calledOnce;
51+
expect(downloadToTmpStub).to.not.have.been.called;
52+
expect(result).to.include("universal-maker-linux-x64");
53+
});
54+
55+
it("should redownload if cached binary fails validation", async () => {
56+
existsSyncStub.returns(true);
57+
// Fail on first call (cache check), succeed on second (downloaded file)
58+
validateSizeStub.onFirstCall().rejects(new Error("Invalid size"));
59+
validateSizeStub.onSecondCall().resolves();
60+
61+
downloadToTmpStub.resolves("/tmp/downloaded_file");
62+
validateChecksumStub.resolves(); // For the new file
63+
64+
const result = await getOrDownloadUniversalMaker();
65+
expect(existsSyncStub).to.have.been.calledOnce;
66+
expect(validateSizeStub).to.have.been.calledTwice;
67+
expect(downloadToTmpStub).to.have.been.calledOnce;
68+
expect(copySyncStub).to.have.been.calledOnce;
69+
expect(result).to.include("universal-maker-linux-x64");
70+
});
71+
72+
it("should download if binary not in cache", async () => {
73+
existsSyncStub.returns(false);
74+
downloadToTmpStub.resolves("/tmp/downloaded_file");
75+
validateSizeStub.resolves();
76+
validateChecksumStub.resolves();
77+
78+
const result = await getOrDownloadUniversalMaker();
79+
expect(existsSyncStub).to.have.been.calledOnce;
80+
expect(downloadToTmpStub).to.have.been.calledOnce;
81+
expect(copySyncStub).to.have.been.calledOnce;
82+
expect(result).to.include("universal-maker-linux-x64");
83+
});
84+
});

src/apphosting/universalMakerDownload.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,22 @@ function getPlatformInfo(): UniversalMakerUpdateDetails {
6060
* Gets the path to the Universal Maker binary, downloading it if necessary.
6161
*/
6262
export async function getOrDownloadUniversalMaker(): Promise<string> {
63-
if (process.env.UNIVERSAL_MAKER_BINARY) {
64-
logger.debug(
65-
`[apphosting] Env variable override detected. Using Universal Maker binary at ${process.env.UNIVERSAL_MAKER_BINARY}`,
66-
);
67-
return process.env.UNIVERSAL_MAKER_BINARY;
68-
}
69-
7063
const details = getPlatformInfo();
7164
const downloadPath = path.join(CACHE_DIR, details.downloadPathRelativeToCacheDir);
7265

7366
const hasBinary = fs.existsSync(downloadPath);
7467

7568
if (hasBinary) {
7669
logger.debug(`[apphosting] Universal Maker binary found at cache: ${downloadPath}`);
77-
return downloadPath;
70+
try {
71+
await downloadUtils.validateSize(downloadPath, details.expectedSize);
72+
await downloadUtils.validateChecksum(downloadPath, details.expectedChecksumSHA256, "sha256");
73+
return downloadPath;
74+
} catch (err) {
75+
logger.warn(
76+
`[apphosting] Cached Universal Maker binary failed verification: ${(err as Error).message}. Proceeding to redownload...`,
77+
);
78+
}
7879
}
7980

8081
logger.info(

src/downloadUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { FirebaseError } from "./error";
1313
* @param remoteUrl URL to download.
1414
* @param auth Whether to include an access token in the download request. Defaults to false.
1515
*/
16-
export async function downloadToTmp(remoteUrl: string, auth: boolean = false): Promise<string> {
16+
export async function downloadToTmp(remoteUrl: string, auth = false): Promise<string> {
1717
const u = new URL(remoteUrl);
1818
const c = new Client({ urlPrefix: u.origin, auth });
1919
const tmpfile = tmp.fileSync();

0 commit comments

Comments
 (0)