Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 21 additions & 0 deletions src/__tests__/scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,27 @@ describe('MISSING_TLS rule', () => {
const findings = runAuthRule(RuleId.MISSING_TLS, config);
expect(findings).toHaveLength(0);
});

it('does not fire for a secure wss:// URL (INSECURE_TRANSPORT owns WebSocket scope)', () => {
// A wss:// endpoint is TLS-encrypted, but parseConfig leaves transport.tls
// false for non-https schemes. Without the WebSocket guard this would be a
// "plain HTTP" false positive on a perfectly secure server.
const config: ParsedMcpConfig = {
serverUrl: 'wss://secure.example.com/mcp',
transport: { url: 'wss://secure.example.com/mcp', tls: false },
};
const findings = runAuthRule(RuleId.MISSING_TLS, config);
expect(findings).toHaveLength(0);
});

it('does not fire for a ws:// URL (reported by INSECURE_TRANSPORT instead)', () => {
const config: ParsedMcpConfig = {
serverUrl: 'ws://example.com/mcp',
transport: { url: 'ws://example.com/mcp', tls: false },
};
const findings = runAuthRule(RuleId.MISSING_TLS, config);
expect(findings).toHaveLength(0);
});
});

// ─── WILDCARD_CORS ────────────────────────────────────────────────────────────
Expand Down
13 changes: 12 additions & 1 deletion src/rules/auth-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,18 @@ export const authRules: Rule[] = [
const url = config.transport?.url ?? config.serverUrl;
const isTls = config.transport?.tls;

if (url && (url.startsWith('http://') || (!isTls && !url.startsWith('https://')))) {
// WebSocket transports are out of scope for this rule: ws:// is reported by
// INSECURE_TRANSPORT, and wss:// is already TLS-encrypted. Without this guard a
// secure wss:// endpoint is wrongly flagged as "plain HTTP" because it neither
// starts with https:// nor sets transport.tls.
const isWebSocket =
!!url && (url.startsWith('ws://') || url.startsWith('wss://'));

if (
url &&
!isWebSocket &&
(url.startsWith('http://') || (!isTls && !url.startsWith('https://')))
) {
return [
{
ruleId: RuleId.MISSING_TLS,
Expand Down
Loading