mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
fix(gitlab): sanitize PAT auth failures across cli and web
This commit is contained in:
1 parent
4e738ee021
commit
7f1c23607d
4 files changed
+171
-21
No files matched your search
@@ -0,0 +1,90 @@
|
|||||||
|
const GITLAB_PAT_ERROR_DETAIL_MAX_LENGTH = 500;
|
||||||
|
const GITLAB_PAT_ERROR_DETAIL_TRUNCATION_SUFFIX = '...[truncated]';
|
||||||
|
const HTML_ERROR_RESPONSE_OMITTED = '[HTML error response omitted]';
|
||||||
|
|
||||||
|
function sanitizeGitLabPatErrorDetail(
|
||||||
|
detail: string | undefined,
|
||||||
|
submittedToken?: string
|
||||||
|
): string | undefined {
|
||||||
|
const trimmed = detail?.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^<!doctype html/i.test(trimmed) || /^<html/i.test(trimmed) || /^<[^>]+>/.test(trimmed)) {
|
||||||
|
return HTML_ERROR_RESPONSE_OMITTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sanitized = trimmed.replace(
|
||||||
|
/"(access[_-]?token|refresh[_-]?token|authorization|cookie|set-cookie|api[_-]?key|session[_-]?token|token|personal_access_token)"\s*:\s*"[^"]*"/gi,
|
||||||
|
'"$1":"[redacted]"'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (submittedToken) {
|
||||||
|
sanitized = sanitized.split(submittedToken).join('[redacted]');
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized = sanitized
|
||||||
|
.replace(/glpat-[A-Za-z0-9._-]+/gi, '[redacted]')
|
||||||
|
.replace(/Bearer\s+[A-Za-z0-9._-]+/g, 'Bearer [redacted]')
|
||||||
|
.replace(/\s+/g, ' ');
|
||||||
|
|
||||||
|
if (sanitized.length > GITLAB_PAT_ERROR_DETAIL_MAX_LENGTH) {
|
||||||
|
sanitized = `${sanitized.slice(
|
||||||
|
0,
|
||||||
|
GITLAB_PAT_ERROR_DETAIL_MAX_LENGTH - GITLAB_PAT_ERROR_DETAIL_TRUNCATION_SUFFIX.length
|
||||||
|
)}${GITLAB_PAT_ERROR_DETAIL_TRUNCATION_SUFFIX}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseGitLabPatAuthResponse(
|
||||||
|
responseOk: boolean,
|
||||||
|
responseStatus: number,
|
||||||
|
responseBody: string,
|
||||||
|
submittedToken?: string
|
||||||
|
):
|
||||||
|
| { ok: true; payload: Record<string, unknown> }
|
||||||
|
| { ok: false; payload: Record<string, unknown>; errorMessage: string } {
|
||||||
|
const trimmedBody = responseBody.trim();
|
||||||
|
let payload: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (trimmedBody) {
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(trimmedBody) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
payload = {
|
||||||
|
error:
|
||||||
|
sanitizeGitLabPatErrorDetail(trimmedBody, submittedToken) ||
|
||||||
|
`GitLab PAT login failed with status ${responseStatus}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadError =
|
||||||
|
typeof payload.error === 'string'
|
||||||
|
? sanitizeGitLabPatErrorDetail(payload.error, submittedToken)
|
||||||
|
: undefined;
|
||||||
|
const fallbackError =
|
||||||
|
sanitizeGitLabPatErrorDetail(trimmedBody, submittedToken) ||
|
||||||
|
`GitLab PAT login failed with status ${responseStatus}`;
|
||||||
|
|
||||||
|
if (!responseOk) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
payload,
|
||||||
|
errorMessage: payloadError || fallbackError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.status !== 'ok') {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
payload,
|
||||||
|
errorMessage: payloadError || fallbackError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, payload };
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
} from './token-manager';
|
} from './token-manager';
|
||||||
import { executeOAuthProcess } from './oauth-process';
|
import { executeOAuthProcess } from './oauth-process';
|
||||||
import { importKiroToken } from './kiro-import';
|
import { importKiroToken } from './kiro-import';
|
||||||
|
import { parseGitLabPatAuthResponse } from './gitlab-pat-response';
|
||||||
import {
|
import {
|
||||||
getProxyTarget,
|
getProxyTarget,
|
||||||
buildProxyUrl,
|
buildProxyUrl,
|
||||||
@@ -723,22 +724,16 @@ async function handleGitLabPatLogin(
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseBody = (await response.text()).trim();
|
const responseBody = await response.text();
|
||||||
let payload: Record<string, unknown> = {};
|
const parsedResponse = parseGitLabPatAuthResponse(
|
||||||
if (responseBody) {
|
response.ok,
|
||||||
try {
|
response.status,
|
||||||
payload = JSON.parse(responseBody) as Record<string, unknown>;
|
responseBody,
|
||||||
} catch {
|
token
|
||||||
payload = { error: responseBody };
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok || payload.status !== 'ok') {
|
if (!parsedResponse.ok) {
|
||||||
const errorMessage =
|
console.log(fail(parsedResponse.errorMessage));
|
||||||
(typeof payload.error === 'string' && payload.error) ||
|
|
||||||
responseBody ||
|
|
||||||
`GitLab PAT login failed with status ${response.status}`;
|
|
||||||
console.log(fail(errorMessage));
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
listProviderTokenSnapshots,
|
listProviderTokenSnapshots,
|
||||||
registerAccountFromToken,
|
registerAccountFromToken,
|
||||||
} from '../../cliproxy/auth/token-manager';
|
} from '../../cliproxy/auth/token-manager';
|
||||||
|
import { parseGitLabPatAuthResponse } from '../../cliproxy/auth/gitlab-pat-response';
|
||||||
import {
|
import {
|
||||||
CLIPROXY_CALLBACK_PROVIDER_MAP,
|
CLIPROXY_CALLBACK_PROVIDER_MAP,
|
||||||
CLIPROXY_AUTH_URL_PROVIDER_MAP,
|
CLIPROXY_AUTH_URL_PROVIDER_MAP,
|
||||||
@@ -706,13 +707,16 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = (await response.json().catch(() => ({}))) as {
|
const responseBody = await response.text();
|
||||||
status?: string;
|
const parsedResponse = parseGitLabPatAuthResponse(
|
||||||
error?: string;
|
response.ok,
|
||||||
};
|
response.status,
|
||||||
if (!response.ok || data.status !== 'ok') {
|
responseBody,
|
||||||
|
gitlabPersonalAccessToken
|
||||||
|
);
|
||||||
|
if (!parsedResponse.ok) {
|
||||||
res.status(response.ok ? 400 : response.status).json({
|
res.status(response.ok ? 400 : response.status).json({
|
||||||
error: data.error || 'GitLab PAT authentication failed',
|
error: parsedResponse.errorMessage || 'GitLab PAT authentication failed',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { parseGitLabPatAuthResponse } from '../../../src/cliproxy/auth/gitlab-pat-response';
|
||||||
|
|
||||||
|
describe('parseGitLabPatAuthResponse', () => {
|
||||||
|
it('sanitizes HTML error bodies for non-ok responses', () => {
|
||||||
|
const result = parseGitLabPatAuthResponse(
|
||||||
|
false,
|
||||||
|
502,
|
||||||
|
'<html><body>gateway error</body></html>',
|
||||||
|
'glpat-secret-token'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.errorMessage).toBe('[HTML error response omitted]');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sanitizes reflected PAT tokens from structured error payloads', () => {
|
||||||
|
const result = parseGitLabPatAuthResponse(
|
||||||
|
false,
|
||||||
|
400,
|
||||||
|
JSON.stringify({ status: 'error', error: 'Rejected token glpat-secret-token for login' }),
|
||||||
|
'glpat-secret-token'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.errorMessage).toContain('[redacted]');
|
||||||
|
expect(result.errorMessage).not.toContain('glpat-secret-token');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects ok responses whose body is not valid success JSON', () => {
|
||||||
|
const result = parseGitLabPatAuthResponse(
|
||||||
|
true,
|
||||||
|
200,
|
||||||
|
'upstream temporarily unavailable',
|
||||||
|
'glpat-secret-token'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.errorMessage).toContain('upstream temporarily unavailable');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts explicit success payloads', () => {
|
||||||
|
const result = parseGitLabPatAuthResponse(
|
||||||
|
true,
|
||||||
|
200,
|
||||||
|
JSON.stringify({ status: 'ok', saved_path: '/tmp/gitlab.json' }),
|
||||||
|
'glpat-secret-token'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.payload.status).toBe('ok');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user