Fix 7 critical security bugs and 1 high-severity data loss bug
- CSRF: HMAC-signed double-submit cookie (server-bound, not raw compare)
- Keychain: execFileSync with arg arrays to prevent command injection
- Caddy config: always use structured generation, never accept raw config
- Templates: replace {{GENERATED_SECRET}} with crypto.randomBytes
- Caddyfile removal: move regex inside ctx.caddy.modify() to fix TOCTOU race
- Credentials: proper-lockfile for all file operations, fix key rotation
to decrypt with old key before generating new key
- Service removal: filter by ID only, not AND with appTemplate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* Falls back to encrypted file storage if keychain is unavailable
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { execSync, execFileSync } = require('child_process');
|
||||
const os = require('os');
|
||||
const crypto = require('crypto');
|
||||
|
||||
@@ -131,53 +131,41 @@ class KeychainManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Windows Credential Manager implementation
|
||||
// Windows Credential Manager implementation (uses execFileSync to prevent injection)
|
||||
async storeWindows(account, value) {
|
||||
const escapedValue = value.replace(/"/g, '""');
|
||||
const script = `
|
||||
$password = ConvertTo-SecureString -String "${escapedValue}" -AsPlainText -Force
|
||||
$credential = New-Object System.Management.Automation.PSCredential("${account}", $password)
|
||||
cmdkey /generic:"${SERVICE_NAME}:${account}" /user:"${account}" /pass:"${escapedValue}"
|
||||
`;
|
||||
execSync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, { stdio: 'ignore' });
|
||||
execFileSync('cmdkey', [`/generic:${SERVICE_NAME}:${account}`, `/user:${account}`, `/pass:${value}`], { stdio: 'ignore' });
|
||||
return true;
|
||||
}
|
||||
|
||||
async retrieveWindows(account) {
|
||||
try {
|
||||
const script = `
|
||||
$cred = cmdkey /list:"${SERVICE_NAME}:${account}"
|
||||
if ($cred -match "Password: (.+)") { $matches[1] }
|
||||
`;
|
||||
const result = execSync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, { encoding: 'utf8' });
|
||||
return result.trim() || null;
|
||||
const result = execFileSync('cmdkey', [`/list:${SERVICE_NAME}:${account}`], { encoding: 'utf8' });
|
||||
const match = result.match(/Password:\s*(.+)/);
|
||||
return match ? match[1].trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteWindows(account) {
|
||||
execSync(`cmdkey /delete:"${SERVICE_NAME}:${account}"`, { stdio: 'ignore' });
|
||||
execFileSync('cmdkey', [`/delete:${SERVICE_NAME}:${account}`], { stdio: 'ignore' });
|
||||
return true;
|
||||
}
|
||||
|
||||
// macOS Keychain implementation
|
||||
// macOS Keychain implementation (uses execFileSync to prevent injection)
|
||||
async storeMacOS(account, value) {
|
||||
// Delete existing entry first
|
||||
try {
|
||||
execSync(`security delete-generic-password -s "${SERVICE_NAME}" -a "${account}"`, { stdio: 'ignore' });
|
||||
execFileSync('security', ['delete-generic-password', '-s', SERVICE_NAME, '-a', account], { stdio: 'ignore' });
|
||||
} catch {
|
||||
// Ignore if doesn't exist
|
||||
}
|
||||
|
||||
// Add new entry
|
||||
execSync(`security add-generic-password -s "${SERVICE_NAME}" -a "${account}" -w "${value}"`, { stdio: 'ignore' });
|
||||
execFileSync('security', ['add-generic-password', '-s', SERVICE_NAME, '-a', account, '-w', value], { stdio: 'ignore' });
|
||||
return true;
|
||||
}
|
||||
|
||||
async retrieveMacOS(account) {
|
||||
try {
|
||||
const result = execSync(`security find-generic-password -s "${SERVICE_NAME}" -a "${account}" -w`, { encoding: 'utf8' });
|
||||
const result = execFileSync('security', ['find-generic-password', '-s', SERVICE_NAME, '-a', account, '-w'], { encoding: 'utf8' });
|
||||
return result.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -185,38 +173,26 @@ class KeychainManager {
|
||||
}
|
||||
|
||||
async deleteMacOS(account) {
|
||||
execSync(`security delete-generic-password -s "${SERVICE_NAME}" -a "${account}"`, { stdio: 'ignore' });
|
||||
execFileSync('security', ['delete-generic-password', '-s', SERVICE_NAME, '-a', account], { stdio: 'ignore' });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Linux Secret Service implementation
|
||||
// Linux Secret Service implementation (uses execFileSync + stdin to prevent injection)
|
||||
async storeLinux(account, value) {
|
||||
try {
|
||||
// Try secret-tool first (libsecret)
|
||||
execSync(`secret-tool store --label="${SERVICE_NAME}:${account}" service "${SERVICE_NAME}" account "${account}"`, {
|
||||
execFileSync('secret-tool', ['store', `--label=${SERVICE_NAME}:${account}`, 'service', SERVICE_NAME, 'account', account], {
|
||||
input: value,
|
||||
stdio: ['pipe', 'ignore', 'ignore']
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
// Fallback to gnome-keyring if available
|
||||
try {
|
||||
const script = `
|
||||
echo "${value}" | gnome-keyring-daemon --unlock
|
||||
echo "${value}" | gnome-keyring --set-password "${SERVICE_NAME}:${account}"
|
||||
`;
|
||||
execSync(script, { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async retrieveLinux(account) {
|
||||
try {
|
||||
// Try secret-tool first
|
||||
const result = execSync(`secret-tool lookup service "${SERVICE_NAME}" account "${account}"`, { encoding: 'utf8' });
|
||||
const result = execFileSync('secret-tool', ['lookup', 'service', SERVICE_NAME, 'account', account], { encoding: 'utf8' });
|
||||
return result.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -225,7 +201,7 @@ class KeychainManager {
|
||||
|
||||
async deleteLinux(account) {
|
||||
try {
|
||||
execSync(`secret-tool clear service "${SERVICE_NAME}" account "${account}"`, { stdio: 'ignore' });
|
||||
execFileSync('secret-tool', ['clear', 'service', SERVICE_NAME, 'account', account], { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user