Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -6937,6 +6937,8 @@ function readClaudeSettingsInfo() {
exists: !!readResult.exists,
targetPath: CLAUDE_SETTINGS_FILE,
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
authToken: typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN : '',
useKey: typeof env.CLAUDE_CODE_USE_KEY === 'string' ? env.CLAUDE_CODE_USE_KEY : '',
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
env
Comment on lines +6940 to 6944

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid returning raw Claude token credentials in API responses

Line 6940–Line 6944 now expose additional secret values (ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_USE_KEY) and the full env object. This expands credential leakage risk on unauthenticated Web UI/API surfaces. Return presence/masked metadata instead of raw values.

🔐 Suggested hardening
 function readClaudeSettingsInfo() {
@@
+    const apiKey = typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY.trim() : '';
+    const authToken = typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN.trim() : '';
+    const useKey = typeof env.CLAUDE_CODE_USE_KEY === 'string' ? env.CLAUDE_CODE_USE_KEY.trim() : '';
+    const baseUrl = typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '';
+    const model = typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '';
+
     return {
         exists: !!readResult.exists,
         targetPath: CLAUDE_SETTINGS_FILE,
-        apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
-        authToken: typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN : '',
-        useKey: typeof env.CLAUDE_CODE_USE_KEY === 'string' ? env.CLAUDE_CODE_USE_KEY : '',
-        baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
-        model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
-        env
+        apiKey: '',
+        authToken: '',
+        useKey: '',
+        hasApiKey: !!apiKey,
+        hasAuthToken: !!authToken,
+        hasUseKey: !!useKey,
+        baseUrl,
+        model,
+        env: {
+            ANTHROPIC_BASE_URL: baseUrl,
+            ANTHROPIC_MODEL: model
+        }
     };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 6940 - 6944, The response object is exposing raw secrets
(authToken, useKey) and the entire env; instead return safe metadata: replace
authToken and useKey values with masked indicators (e.g., boolean presence flags
or a masked string like "<redacted>" or token length), remove returning the full
env object, and keep baseUrl/model only if non-sensitive (or similarly mask
them); update the code that constructs this response (the object with keys
authToken, useKey, baseUrl, model, env) to emit non-secret metadata instead of
raw credentials.

Expand Down
78 changes: 77 additions & 1 deletion tests/unit/claude-settings-sync.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ test('ensureClaudeConfigFromSettings creates imported config for unmatched Claud
apiKey: 'maxx-key',
baseUrl: 'https://maxx-direct.cloverstd.com/project/ym/111',
model: 'claude-opus-4-6',
hasKey: true
hasKey: true,
externalCredentialType: ''
});
});

Expand Down Expand Up @@ -290,3 +291,78 @@ test('refreshClaudeSelectionFromSettings selects imported config when settings m
assert.strictEqual(refreshCount, 1);
assert.deepStrictEqual(messages, []);
});

test('ensureClaudeConfigFromSettings imports external auth-token backed Claude settings', () => {
const source = extractMethodAsFunction(appSource, 'ensureClaudeConfigFromSettings');
const ensureClaudeConfigFromSettings = instantiateFunction(source, 'ensureClaudeConfigFromSettings');

let saveCount = 0;
const context = {
claudeConfigs: {},
normalizeClaudeSettingsEnv: (env = {}) => ({
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY.trim() : '',
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL.trim() : '',
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL.trim() : '',
authToken: typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN.trim() : '',
useKey: typeof env.CLAUDE_CODE_USE_KEY === 'string' ? env.CLAUDE_CODE_USE_KEY.trim() : ''
}),
findDuplicateClaudeConfigName: () => '',
buildClaudeImportedConfigName: () => '导入-api.anthropic.com',
saveClaudeConfigs: () => {
saveCount += 1;
}
};

const result = ensureClaudeConfigFromSettings.call(context, {
ANTHROPIC_AUTH_TOKEN: 'anth-token',
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
ANTHROPIC_MODEL: 'claude-3-7-sonnet'
});

assert.strictEqual(result, '导入-api.anthropic.com');
assert.strictEqual(saveCount, 1);
assert.deepStrictEqual(context.claudeConfigs[result], {
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-3-7-sonnet',
hasKey: true,
externalCredentialType: 'auth-token'
});
});

test('applyClaudeConfig reports informative message for external credential only config', async () => {
const source = extractMethodAsFunction(appSource, 'applyClaudeConfig');
const applyClaudeConfig = instantiateFunction(source, 'applyClaudeConfig', {
api: async () => {
throw new Error('should not call apply api without apiKey');
}
});

const messages = [];
let refreshCount = 0;
const context = {
claudeConfigs: {
imported: {
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-3-7-sonnet',
hasKey: true,
externalCredentialType: 'auth-token'
}
},
currentClaudeConfig: '',
refreshClaudeModelContext: () => {
refreshCount += 1;
},
showMessage: (msg, type) => {
messages.push({ msg, type });
return { msg, type };
}
};

const result = await applyClaudeConfig.call(context, 'imported');
assert.strictEqual(context.currentClaudeConfig, 'imported');
assert.strictEqual(refreshCount, 1);
assert.deepStrictEqual(messages, [{ msg: '检测到外部 Claude 认证状态;当前仅支持展示,若需由 codexmate 接管请补充 API Key', type: 'info' }]);
assert.deepStrictEqual(result, messages[0]);
});
36 changes: 33 additions & 3 deletions tests/unit/web-ui-logic.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,31 @@ test('normalizeClaudeConfig trims all fields', () => {
});

test('normalizeClaudeSettingsEnv trims settings env', () => {
const env = { ANTHROPIC_API_KEY: ' key ', ANTHROPIC_BASE_URL: ' url ', ANTHROPIC_MODEL: ' model ' };
assert.deepStrictEqual(normalizeClaudeSettingsEnv(env), { apiKey: 'key', baseUrl: 'url', model: 'model' });
const env = {
ANTHROPIC_API_KEY: ' key ',
ANTHROPIC_BASE_URL: ' url ',
ANTHROPIC_MODEL: ' model ',
ANTHROPIC_AUTH_TOKEN: ' token ',
CLAUDE_CODE_USE_KEY: ' true '
};
assert.deepStrictEqual(normalizeClaudeSettingsEnv(env), {
apiKey: 'key',
baseUrl: 'url',
model: 'model',
authToken: 'token',
useKey: 'true'
});
});

test('normalizeClaudeSettingsEnv fills missing fields with empty strings', () => {
const env = { ANTHROPIC_API_KEY: 'k' };
assert.deepStrictEqual(normalizeClaudeSettingsEnv(env), { apiKey: 'k', baseUrl: '', model: '' });
assert.deepStrictEqual(normalizeClaudeSettingsEnv(env), {
apiKey: 'k',
baseUrl: '',
model: '',
authToken: '',
useKey: ''
});
});

test('matchClaudeConfigFromSettings matches identical config', () => {
Expand All @@ -52,6 +70,18 @@ test('matchClaudeConfigFromSettings matches identical config', () => {
assert.strictEqual(matchClaudeConfigFromSettings(configs, env), 'default');
});

test('matchClaudeConfigFromSettings tolerates trailing slash differences', () => {
const configs = { default: { apiKey: 'k', baseUrl: 'https://example.com/anthropic/', model: 'm' } };
const env = { ANTHROPIC_API_KEY: 'k', ANTHROPIC_BASE_URL: 'https://example.com/anthropic', ANTHROPIC_MODEL: 'm' };
assert.strictEqual(matchClaudeConfigFromSettings(configs, env), 'default');
});

test('matchClaudeConfigFromSettings matches external token-backed config by baseUrl and model', () => {
const configs = { imported: { apiKey: '', baseUrl: 'https://example.com/anthropic/', model: 'm', externalCredentialType: 'auth-token' } };
const env = { ANTHROPIC_AUTH_TOKEN: 'token', ANTHROPIC_BASE_URL: 'https://example.com/anthropic', ANTHROPIC_MODEL: 'm' };
assert.strictEqual(matchClaudeConfigFromSettings(configs, env), 'imported');
});

test('matchClaudeConfigFromSettings returns empty when incomplete', () => {
const configs = { default: { apiKey: 'k', baseUrl: 'u', model: 'm' } };
const env = { ANTHROPIC_API_KEY: 'k', ANTHROPIC_BASE_URL: '', ANTHROPIC_MODEL: 'm' };
Expand Down
Loading
Loading