fix: CSRF token accessibility and hook installation status

- Remove HttpOnly from XSRF-TOKEN cookie for JavaScript readability
- Add hook installation status detection in system settings API
- Update InjectionControlTab to show installed hooks status
- Add brace expansion support in globToRegex utility
This commit is contained in:
catlog22
2026-03-01 23:17:37 +08:00
parent ffe3b427ce
commit 5cab8ae8a5
11 changed files with 80 additions and 21 deletions

View File

@@ -51,7 +51,8 @@ function setCsrfCookie(res: ServerResponse, token: string, maxAgeSeconds: number
const attributes = [
`XSRF-TOKEN=${encodeURIComponent(token)}`,
'Path=/',
'HttpOnly',
// Note: XSRF-TOKEN must be readable by JavaScript for CSRF protection to work
// The token is also sent via X-CSRF-Token header, so not having HttpOnly is safe
'SameSite=Strict',
`Max-Age=${maxAgeSeconds}`,
];

View File

@@ -71,7 +71,8 @@ function setCsrfCookie(res: ServerResponse, token: string, maxAgeSeconds: number
const attributes = [
`XSRF-TOKEN=${encodeURIComponent(token)}`,
'Path=/',
'HttpOnly',
// Note: XSRF-TOKEN must be readable by JavaScript for CSRF protection to work
// The token is also sent via X-CSRF-Token header, so not having HttpOnly is safe
'SameSite=Strict',
`Max-Age=${maxAgeSeconds}`,
];

View File

@@ -90,6 +90,27 @@ function readSettingsFile(filePath: string): Record<string, unknown> {
}
}
/**
* Check if a recommended hook is installed in settings
*/
function isHookInstalled(
settings: Record<string, unknown> & { hooks?: Record<string, unknown[]> },
hook: typeof RECOMMENDED_HOOKS[number]
): boolean {
const hooks = settings.hooks;
if (!hooks) return false;
const eventHooks = hooks[hook.event];
if (!eventHooks || !Array.isArray(eventHooks)) return false;
// Check if hook exists in nested hooks array (by command)
return eventHooks.some((entry) => {
const entryHooks = (entry as Record<string, unknown>).hooks as Array<Record<string, unknown>> | undefined;
if (!entryHooks || !Array.isArray(entryHooks)) return false;
return entryHooks.some((h) => (h as Record<string, unknown>).command === hook.command);
});
}
/**
* Get system settings from global settings file
*/
@@ -97,12 +118,18 @@ function getSystemSettings(): {
injectionControl: typeof DEFAULT_INJECTION_CONTROL;
personalSpecDefaults: typeof DEFAULT_PERSONAL_SPEC_DEFAULTS;
devProgressInjection: typeof DEFAULT_DEV_PROGRESS_INJECTION;
recommendedHooks: typeof RECOMMENDED_HOOKS;
recommendedHooks: Array<typeof RECOMMENDED_HOOKS[number] & { installed: boolean }>;
} {
const settings = readSettingsFile(GLOBAL_SETTINGS_PATH) as Record<string, unknown>;
const settings = readSettingsFile(GLOBAL_SETTINGS_PATH) as Record<string, unknown> & { hooks?: Record<string, unknown[]> };
const system = (settings.system || {}) as Record<string, unknown>;
const user = (settings.user || {}) as Record<string, unknown>;
// Check installation status for each recommended hook
const recommendedHooksWithStatus = RECOMMENDED_HOOKS.map(hook => ({
...hook,
installed: isHookInstalled(settings, hook)
}));
return {
injectionControl: {
...DEFAULT_INJECTION_CONTROL,
@@ -116,7 +143,7 @@ function getSystemSettings(): {
...DEFAULT_DEV_PROGRESS_INJECTION,
...((system.devProgressInjection || {}) as Record<string, unknown>)
} as typeof DEFAULT_DEV_PROGRESS_INJECTION,
recommendedHooks: RECOMMENDED_HOOKS
recommendedHooks: recommendedHooksWithStatus
};
}

View File

@@ -257,7 +257,8 @@ function setCsrfCookie(res: http.ServerResponse, token: string, maxAgeSeconds: n
const attributes = [
`XSRF-TOKEN=${encodeURIComponent(token)}`,
'Path=/',
'HttpOnly',
// Note: XSRF-TOKEN must be readable by JavaScript for CSRF protection to work
// The token is also sent via X-CSRF-Token header, so not having HttpOnly is safe
'SameSite=Strict',
`Max-Age=${maxAgeSeconds}`,
];

View File

@@ -64,8 +64,25 @@ export function isBinaryFile(filePath: string): boolean {
/**
* Convert glob pattern to regex
* Supports: *, ?, and brace expansion {a,b,c}
*/
export function globToRegex(pattern: string): RegExp {
// Handle brace expansion: *.{md,json,ts} -> (?:.*\.md|.*\.json|.*\.ts)
const braceMatch = pattern.match(/^(.*)\{([^}]+)\}(.*)$/);
if (braceMatch) {
const [, prefix, options, suffix] = braceMatch;
const optionList = options.split(',').map(opt => `${prefix}${opt}${suffix}`);
// Create a regex that matches any of the expanded patterns
const expandedPatterns = optionList.map(opt => {
return opt
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
});
return new RegExp(`^(?:${expandedPatterns.join('|')})$`, 'i');
}
// Standard glob conversion
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')