Skip to content

Commit dba94a2

Browse files
authored
fix(NO_AUTH): treat auth.type "none"/"disabled" as unauthenticated (#24)
1 parent 3a01019 commit dba94a2

3 files changed

Lines changed: 60 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Fixed
11+
- `NO_AUTH`: fixed a CRITICAL false negative where declaring
12+
`transport.auth.type: "none"` (or `disabled`/`off`/`false`/`anonymous`)
13+
satisfied the auth check, so an explicitly unauthenticated server passed the
14+
gate. An auth `type` now only counts as authentication when it names a real
15+
mechanism; placeholder/disabled values are treated as no auth.
1116
- Restored the test suite / green CI: `import.meta.url` in `src/sarif.ts` could
1217
not compile under the Jest `module: CommonJS` transform, so the `sarif` and
1318
`scanner` test suites silently failed to run (and the test gate had been

src/__tests__/scanner.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,44 @@ describe('NO_AUTH rule', () => {
6262
const findings = runAuthRule(RuleId.NO_AUTH, config);
6363
expect(findings).toHaveLength(1);
6464
});
65+
66+
it('fires when auth.type explicitly disables auth ("none")', () => {
67+
const config: ParsedMcpConfig = {
68+
serverUrl: 'https://example.com',
69+
transport: { url: 'https://example.com', tls: true, auth: { type: 'none' } },
70+
};
71+
const findings = runAuthRule(RuleId.NO_AUTH, config);
72+
expect(findings).toHaveLength(1);
73+
expect(findings[0].severity).toBe(Severity.CRITICAL);
74+
expect(findings[0].evidence).toContain('none');
75+
});
76+
77+
it.each(['none', 'None', 'NONE', ' disabled ', 'off', 'false', 'anonymous'])(
78+
'fires when auth.type is the disabled value "%s"',
79+
(type) => {
80+
const config: ParsedMcpConfig = {
81+
transport: { auth: { type } },
82+
};
83+
const findings = runAuthRule(RuleId.NO_AUTH, config);
84+
expect(findings).toHaveLength(1);
85+
}
86+
);
87+
88+
it('does NOT fire for a meaningful auth.type (e.g. "bearer")', () => {
89+
const config: ParsedMcpConfig = {
90+
transport: { auth: { type: 'bearer' } },
91+
};
92+
const findings = runAuthRule(RuleId.NO_AUTH, config);
93+
expect(findings).toHaveLength(0);
94+
});
95+
96+
it('does NOT fire when a credential is present even if type says "none"', () => {
97+
const config: ParsedMcpConfig = {
98+
transport: { auth: { type: 'none', token: 'a'.repeat(40) } },
99+
};
100+
const findings = runAuthRule(RuleId.NO_AUTH, config);
101+
expect(findings).toHaveLength(0);
102+
});
65103
});
66104

67105
// ─── WEAK_API_KEY ─────────────────────────────────────────────────────────────

src/rules/auth-rules.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,26 @@ import { Finding, RuleId, Severity } from '../types.js';
22
import { ParsedMcpConfig } from '../parser.js';
33
import { Rule } from './index.js';
44

5+
/**
6+
* Auth `type` values that explicitly mean "no authentication". A config that
7+
* declares one of these is unauthenticated, even though the `auth` block exists.
8+
*/
9+
const DISABLED_AUTH_TYPES = new Set(['none', 'disabled', 'off', 'false', 'anonymous']);
10+
511
export const authRules: Rule[] = [
612
{
713
id: RuleId.NO_AUTH,
814
severity: Severity.CRITICAL,
915
title: 'No Authentication Configured',
1016
check(config: ParsedMcpConfig): Finding[] {
11-
const hasAuth = config.transport?.auth &&
12-
(config.transport.auth.apiKey ||
13-
config.transport.auth.token ||
14-
config.transport.auth.type);
17+
const auth = config.transport?.auth;
18+
// A real credential always counts as auth.
19+
const hasCredential = Boolean(auth?.apiKey || auth?.token);
20+
// An auth `type` only counts when it names a real mechanism — values like
21+
// "none" or "disabled" explicitly opt out of authentication.
22+
const type = auth?.type?.trim().toLowerCase();
23+
const hasMeaningfulType = Boolean(type) && !DISABLED_AUTH_TYPES.has(type as string);
24+
const hasAuth = hasCredential || hasMeaningfulType;
1525

1626
if (!hasAuth) {
1727
return [
@@ -22,6 +32,9 @@ export const authRules: Rule[] = [
2232
description:
2333
'The MCP server transport has no authentication configured. ' +
2434
'Any client can connect and invoke tools without credentials.',
35+
evidence: hasMeaningfulType === false && Boolean(type)
36+
? `transport.auth.type: "${auth?.type}" (authentication explicitly disabled)`
37+
: 'transport.auth: absent',
2538
remediation:
2639
'Configure authentication in transport.auth (e.g., apiKey, bearer token, or mTLS).',
2740
docsUrl: 'https://hailbytes.com/mcp/docs/rules/NO_AUTH',

0 commit comments

Comments
 (0)