Skip to content

Commit a620a2f

Browse files
authored
Merge commit from fork
fix: all vulnerability issues
2 parents d4d9239 + 60e77d1 commit a620a2f

16 files changed

+1460
-140
lines changed

src/proxy/actions/Action.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class Action {
4848
attestation?: string;
4949
lastStep?: Step;
5050
proxyGitPath?: string;
51+
newIdxFiles?: string[];
5152

5253
/**
5354
* Create an action.

src/proxy/chain.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ const pushActionChain: ((req: any, action: Action) => Promise<Action>)[] = [
99
proc.push.checkCommitMessages,
1010
proc.push.checkAuthorEmails,
1111
proc.push.checkUserPushPermission,
12-
proc.push.checkIfWaitingAuth,
1312
proc.push.pullRemote,
1413
proc.push.writePack,
14+
proc.push.checkHiddenCommits,
15+
proc.push.checkIfWaitingAuth,
16+
proc.push.getMissingData,
1517
proc.push.preReceive,
1618
proc.push.getDiff,
1719
// run before clear remote

src/proxy/processors/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const BRANCH_PREFIX = 'refs/heads/';
2+
export const EMPTY_COMMIT_HASH = '0000000000000000000000000000000000000000';
3+
export const FLUSH_PACKET = '0000';
4+
export const PACK_SIGNATURE = 'PACK';
5+
export const PACKET_SIZE = 4;
6+
export const GIT_OBJECT_TYPE_COMMIT = 1;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import path from 'path';
2+
import { Action, Step } from '../../actions';
3+
import { spawnSync } from 'child_process';
4+
5+
const exec = async (req: any, action: Action): Promise<Action> => {
6+
const step = new Step('checkHiddenCommits');
7+
8+
try {
9+
const repoPath = `${action.proxyGitPath}/${action.repoName}`;
10+
11+
const oldOid = action.commitFrom;
12+
const newOid = action.commitTo;
13+
if (!oldOid || !newOid) {
14+
throw new Error('Both action.commitFrom and action.commitTo must be defined');
15+
}
16+
17+
// build introducedCommits set
18+
const introducedCommits = new Set<string>();
19+
const revRange =
20+
oldOid === '0000000000000000000000000000000000000000' ? newOid : `${oldOid}..${newOid}`;
21+
const revList = spawnSync('git', ['rev-list', revRange], { cwd: repoPath, encoding: 'utf-8' })
22+
.stdout.trim()
23+
.split('\n')
24+
.filter(Boolean);
25+
revList.forEach((sha) => introducedCommits.add(sha));
26+
step.log(`Total introduced commits: ${introducedCommits.size}`);
27+
28+
// build packCommits set
29+
const packPath = path.join('.git', 'objects', 'pack');
30+
const packCommits = new Set<string>();
31+
(action.newIdxFiles || []).forEach((idxFile) => {
32+
const idxPath = path.join(packPath, idxFile);
33+
const out = spawnSync('git', ['verify-pack', '-v', idxPath], {
34+
cwd: repoPath,
35+
encoding: 'utf-8',
36+
})
37+
.stdout.trim()
38+
.split('\n');
39+
out.forEach((line) => {
40+
const [sha, type] = line.split(/\s+/);
41+
if (type === 'commit') packCommits.add(sha);
42+
});
43+
});
44+
step.log(`Total commits in the pack: ${packCommits.size}`);
45+
46+
// subset check
47+
const isSubset = [...packCommits].every((sha) => introducedCommits.has(sha));
48+
if (!isSubset) {
49+
// build detailed lists
50+
const [referenced, unreferenced] = [...packCommits].reduce<[string[], string[]]>(
51+
([ref, unref], sha) =>
52+
introducedCommits.has(sha) ? [[...ref, sha], unref] : [ref, [...unref, sha]],
53+
[[], []],
54+
);
55+
56+
step.log(`Referenced commits: ${referenced.length}`);
57+
step.log(`Unreferenced commits: ${unreferenced.length}`);
58+
59+
step.setError(
60+
`Unreferenced commits in pack (${unreferenced.length}): ${unreferenced.join(', ')}.\n` +
61+
`This usually happens when a branch was made from a commit that hasn't been approved and pushed to the remote.\n` +
62+
`Please get approval on the commits, push them and try again.`,
63+
);
64+
action.error = true;
65+
step.setContent(`Referenced: ${referenced.length}, Unreferenced: ${unreferenced.length}`);
66+
} else {
67+
// all good, no logging of individual SHAs needed
68+
step.log('All pack commits are referenced in the introduced range.');
69+
step.setContent(`All ${packCommits.size} pack commits are within introduced commits.`);
70+
}
71+
} catch (e: any) {
72+
step.setError(e.message);
73+
throw e;
74+
} finally {
75+
action.addStep(step);
76+
}
77+
78+
return action;
79+
};
80+
81+
exec.displayName = 'checkHiddenCommits.exec';
82+
export { exec };

src/proxy/processors/push-action/checkUserPushPermission.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,26 @@ import { trimTrailingDotGit } from '../../../db/helper';
55
// Execute if the repo is approved
66
const exec = async (req: any, action: Action): Promise<Action> => {
77
const step = new Step('checkUserPushPermission');
8+
const user = action.user;
89

10+
if (!user) {
11+
console.log('Action has no user set. This may be due to a fast-forward ref update. Deferring to getMissingData action.');
12+
return action;
13+
}
14+
15+
return await validateUser(user, action, step);
16+
};
17+
18+
/**
19+
* Helper that validates the user's push permission.
20+
* This can be used by other actions that need it. For example, when the user is missing from the commit data,
21+
* validation is deferred to getMissingData, but the logic is the same.
22+
* @param {string} user The user to validate
23+
* @param {Action} action The action object
24+
* @param {Step} step The step object
25+
* @return {Promise<Action>} The action object
26+
*/
27+
const validateUser = async (user: string, action: Action, step: Step): Promise<Action> => {
928
const repoSplit = trimTrailingDotGit(action.repo.toLowerCase()).split('/');
1029
// we expect there to be exactly one / separating org/repoName
1130
if (repoSplit.length != 2) {
@@ -16,7 +35,6 @@ const exec = async (req: any, action: Action): Promise<Action> => {
1635
// pull the 2nd value of the split for repoName
1736
const repoName = repoSplit[1];
1837
let isUserAllowed = false;
19-
let user = action.user;
2038

2139
// Find the user associated with this Git Account
2240
const list = await getUsers({ gitAccount: action.user });
@@ -53,4 +71,4 @@ const exec = async (req: any, action: Action): Promise<Action> => {
5371

5472
exec.displayName = 'checkUserPushPermission.exec';
5573

56-
export { exec };
74+
export { exec, validateUser };

src/proxy/processors/push-action/getDiff.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Action, Step } from '../../actions';
22
import simpleGit from 'simple-git';
33

4+
import { EMPTY_COMMIT_HASH } from '../constants';
5+
46
const exec = async (req: any, action: Action): Promise<Action> => {
57
const step = new Step('diff');
68

@@ -11,11 +13,15 @@ const exec = async (req: any, action: Action): Promise<Action> => {
1113
let commitFrom = `4b825dc642cb6eb9a060e54bf8d69288fbee4904`;
1214

1315
if (!action.commitData || action.commitData.length === 0) {
14-
throw new Error('No commit data found');
16+
step.error = true;
17+
step.log('No commitData found');
18+
step.setError('Your push has been blocked because no commit data was found.');
19+
action.addStep(step);
20+
return action;
1521
}
1622

17-
if (action.commitFrom === '0000000000000000000000000000000000000000') {
18-
if (action.commitData[0].parent !== '0000000000000000000000000000000000000000') {
23+
if (action.commitFrom === EMPTY_COMMIT_HASH) {
24+
if (action.commitData[0].parent !== EMPTY_COMMIT_HASH) {
1925
commitFrom = `${action.commitData[action.commitData.length - 1].parent}`;
2026
}
2127
} else {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Action, Step } from '../../actions';
2+
import { validateUser } from './checkUserPushPermission';
3+
import simpleGit from 'simple-git';
4+
import { EMPTY_COMMIT_HASH } from '../constants';
5+
6+
const isEmptyBranch = async (action: Action) => {
7+
const git = simpleGit(`${action.proxyGitPath}/${action.repoName}`);
8+
9+
if (action.commitFrom === EMPTY_COMMIT_HASH) {
10+
try {
11+
const type = await git.raw(['cat-file', '-t', action.commitTo || '']);
12+
const known = type.trim() === 'commit';
13+
if (known) {
14+
return true;
15+
}
16+
} catch (err) {
17+
console.log(`Commit ${action.commitTo} not found: ${err}`);
18+
}
19+
}
20+
21+
return false;
22+
};
23+
24+
const exec = async (req: any, action: Action): Promise<Action> => {
25+
const step = new Step('getMissingData');
26+
27+
if (action.commitData && action.commitData.length > 0) {
28+
console.log('getMissingData', action);
29+
return action;
30+
}
31+
32+
if (await isEmptyBranch(action)) {
33+
step.setError('Push blocked: Empty branch. Please make a commit before pushing a new branch.');
34+
action.addStep(step);
35+
step.error = true;
36+
return action;
37+
}
38+
console.log(`commitData not found, fetching missing commits from git...`);
39+
40+
try {
41+
const path = `${action.proxyGitPath}/${action.repoName}`;
42+
const git = simpleGit(path);
43+
const log = await git.log({ from: action.commitFrom, to: action.commitTo });
44+
45+
action.commitData = [...log.all].reverse().map((entry, i, array) => {
46+
const parent = i === 0 ? action.commitFrom : array[i - 1].hash;
47+
const timestamp = Math.floor(new Date(entry.date).getTime() / 1000).toString();
48+
return {
49+
message: entry.message || '',
50+
committer: entry.author_name || '',
51+
tree: entry.hash || '',
52+
parent: parent || EMPTY_COMMIT_HASH,
53+
author: entry.author_name || '',
54+
authorEmail: entry.author_email || '',
55+
commitTimestamp: timestamp,
56+
}
57+
});
58+
console.log(`Updated commitData:`, { commitData: action.commitData });
59+
60+
if (action.commitFrom === EMPTY_COMMIT_HASH) {
61+
action.commitFrom = action.commitData[action.commitData.length - 1].parent;
62+
}
63+
64+
const user = action.commitData[action.commitData.length - 1].committer;
65+
action.user = user;
66+
} catch (e: any) {
67+
step.setError(e.toString('utf-8'));
68+
} finally {
69+
action.addStep(step);
70+
}
71+
return await validateUser(action.user || '', action, step);
72+
};
73+
74+
exec.displayName = 'getMissingData.exec';
75+
76+
export { exec };

src/proxy/processors/push-action/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { exec as audit } from './audit';
55
import { exec as pullRemote } from './pullRemote';
66
import { exec as writePack } from './writePack';
77
import { exec as getDiff } from './getDiff';
8+
import { exec as checkHiddenCommits } from './checkHiddenCommits';
89
import { exec as gitleaks } from './gitleaks';
910
import { exec as scanDiff } from './scanDiff';
1011
import { exec as blockForAuth } from './blockForAuth';
@@ -13,6 +14,7 @@ import { exec as checkCommitMessages } from './checkCommitMessages';
1314
import { exec as checkAuthorEmails } from './checkAuthorEmails';
1415
import { exec as checkUserPushPermission } from './checkUserPushPermission';
1516
import { exec as clearBareClone } from './clearBareClone';
17+
import { exec as getMissingData } from './getMissingData';
1618

1719
export {
1820
parsePush,
@@ -22,6 +24,7 @@ export {
2224
pullRemote,
2325
writePack,
2426
getDiff,
27+
checkHiddenCommits,
2528
gitleaks,
2629
scanDiff,
2730
blockForAuth,
@@ -30,4 +33,5 @@ export {
3033
checkAuthorEmails,
3134
checkUserPushPermission,
3235
clearBareClone,
36+
getMissingData,
3337
};

0 commit comments

Comments
 (0)