Skip to content
Merged
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
9 changes: 7 additions & 2 deletions .github/workflows/pr-testing-registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ on:
- "6"
default: "2"
ngrok_domain:
description: "Custom ngrok domain (optional, e.g., my-domain.ngrok.io)"
description: "Custom ngrok domain (defaults to frontmcp-pr-{PR_NUM}.ngrok.io)"
required: false
type: string
default: ""
Expand Down Expand Up @@ -173,8 +173,13 @@ jobs:
# Build ngrok arguments array (safe from injection)
declare -a NGROK_ARGS=("http" "4873" "--log=stdout")

# Add domain if provided (already validated above)
# Get domain - use provided value or default to frontmcp-pr-{PR_NUM}.ngrok.io
NGROK_DOMAIN="${{ inputs.ngrok_domain }}"
if [ -z "${NGROK_DOMAIN}" ]; then
NGROK_DOMAIN="frontmcp-pr-${{ inputs.pr_number }}.ngrok.io"
echo "Using default domain: ${NGROK_DOMAIN}"
fi

if [ -n "${NGROK_DOMAIN}" ]; then
if [ -z "${NGROK_AUTH_TOKEN:-}" ]; then
echo "::error::Custom domain requires NGROK_AUTH_TOKEN to be configured"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ libs/*/src/**/*.d.ts
libs/*/src/**/*.d.ts.map
apps/*/src/**/*.d.ts
apps/*/src/**/*.d.ts.map
/apps/demo/.env
84 changes: 84 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Security Policy

FrontMCP takes security seriously. We appreciate your efforts to responsibly disclose vulnerabilities and will make every effort to acknowledge your contributions.

## Reporting a Vulnerability

**Please do not report security vulnerabilities through public GitHub issues.**

Instead, please report them via email to: **[david@frontegg.com](mailto:david@frontegg.com)**

When reporting a vulnerability, please include:

- A clear description of the vulnerability
- Steps to reproduce the issue
- Affected versions
- Potential impact assessment
- Any suggested fixes (if available)

### What to Expect

- **Initial Response**: We aim to acknowledge receipt within 48 hours
- **Status Updates**: We will provide updates on the investigation within 7 days
- **Resolution**: We will work to address confirmed vulnerabilities promptly and coordinate disclosure timing with you

## Supported Versions

| Package | Version | Supported |
| ------------------ | ------- | --------- |
| @frontmcp/sdk | 0.7.x | Yes |
| @frontmcp/auth | 0.7.x | Yes |
| @frontmcp/utils | 0.7.x | Yes |
| @frontmcp/adapters | 0.7.x | Yes |
| @frontmcp/plugins | 0.7.x | Yes |
| frontmcp | 0.7.x | Yes |
| @frontmcp/di | 0.7.x | Yes |
| @frontmcp/testing | 0.7.x | Yes |
| @frontmcp/ui | 0.7.x | Yes |
| @frontmcp/uipack | 0.7.x | Yes |

**Policy**: Only the latest minor version receives security updates. We recommend always using the latest version.

**Runtime**: Node.js 22+ (LTS) is required.

## Security Features

FrontMCP implements security best practices throughout the framework:

### Cryptography

- **Encryption**: AES-256-GCM for symmetric encryption
- **Key Derivation**: HKDF-SHA256 (RFC 5869)
- **Hashing**: SHA-256 via @noble/hashes
- **JWT**: jose library for JSON Web Token handling
- **PKCE**: OAuth PKCE support (RFC 7636)

### Data Protection

- Encrypted session storage
- Encrypted credential vault
- Timing-safe comparisons for sensitive operations
- Cross-platform crypto (Node.js native + browser @noble implementations)

### Code Quality

- 95%+ test coverage requirement
- Strict TypeScript (no `any` types without justification)
- Snyk dependency scanning
- ESLint security rules

## External Packages

The following packages have been moved to their own repositories and maintain separate security policies:

- `ast-guard`
- `vectoriadb`
- `enclave-vm`
- `json-schema-to-zod-v3`
- `mcp-from-openapi`

Please refer to their respective repositories for security-related information.

## Acknowledgments

We thank the security researchers and community members who help keep FrontMCP secure through responsible disclosure.
27 changes: 15 additions & 12 deletions apps/demo/src/apps/expenses/plugins/authorization.plugin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {DynamicPlugin, Plugin, FlowCtxOf, FlowHooksOf} from '@frontmcp/sdk';

import { DynamicPlugin, Plugin, FlowCtxOf, FlowHooksOf } from '@frontmcp/sdk';

const ListToolsHook = FlowHooksOf('tools:list-tools');

declare global {
interface ExtendFrontMcpToolMetadata {
authorization?: AuthorizationToolOptions;
}
interface ExtendFrontMcpToolMetadata {
authorization?: AuthorizationToolOptions;
}
}

export interface AuthorizationPluginOptions {
Expand All @@ -17,7 +16,6 @@ export interface AuthorizationToolOptions {
requiredRoles: string[];
}


@Plugin({
name: 'authorization',
description: 'Role-based access control for tools',
Expand All @@ -29,18 +27,23 @@ export default class AuthorizationPlugin extends DynamicPlugin<AuthorizationPlug

@ListToolsHook.Did('findTools')
async canActivate(flowCtx: FlowCtxOf<'tools:list-tools'>) {
const {tools} = flowCtx.state.required;
const {ctx: {authInfo}} = flowCtx.rawInput
const { tools } = flowCtx.state.required;
const {
ctx: { authInfo },
} = flowCtx.rawInput;

const authorizedTools = tools.filter(({tool}) => {
const authorizedTools = tools.filter(({ tool }) => {
const metadata = tool.metadata;

if (!metadata.authorization) return true;
const {requiredRoles} = metadata.authorization;
const roles = (authInfo.user as any).roles as string[];
const { requiredRoles } = metadata.authorization;
const roles = (authInfo?.user as any)?.roles as string[] | undefined;

// If no roles found, deny access to tools requiring roles
if (!roles || !Array.isArray(roles)) return false;

// check if required roles are present in the user's roles
return requiredRoles.every(role => roles.includes(role));
return requiredRoles.every((role) => roles.includes(role));
});

flowCtx.state.set('tools', authorizedTools);
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import CrmMcpApp from './apps/crm';
port: 3003,
},
transport: {
enableLegacySSE: true,
protocol: 'full',
},
auth: {
mode: 'transparent',
Expand Down
11 changes: 3 additions & 8 deletions apps/e2e/demo-e2e-agents/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ const port = parseInt(process.env['PORT'] ?? '3020', 10);
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
},
transport: {
protocol: { json: true, legacy: true, strictSession: false },
},
})
export default class Server {}
11 changes: 3 additions & 8 deletions apps/e2e/demo-e2e-cache/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ const port = parseInt(process.env['PORT'] ?? '3016', 10);
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
},
transport: {
protocol: { json: true, legacy: true, strictSession: false },
},
})
export default class Server {}
11 changes: 3 additions & 8 deletions apps/e2e/demo-e2e-codecall/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ const port = parseInt(process.env['PORT'] ?? '3013', 10);
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
},
transport: {
protocol: { json: true, legacy: true, strictSession: false },
},
})
export default class Server {}
11 changes: 3 additions & 8 deletions apps/e2e/demo-e2e-config/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ const port = parseInt(process.env['PORT'] ?? '3021', 10);
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
},
transport: {
protocol: { json: true, legacy: true, strictSession: false },
},
})
export default class Server {}
11 changes: 3 additions & 8 deletions apps/e2e/demo-e2e-errors/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ const port = parseInt(process.env['PORT'] ?? '3019', 10);
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
},
transport: {
protocol: { json: true, legacy: true, strictSession: false },
},
})
export default class Server {}
7 changes: 1 addition & 6 deletions apps/e2e/demo-e2e-hooks/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ const port = parseInt(process.env['PORT'] ?? '3018', 10);
logging: { level: LogLevel.Verbose },
http: { port },
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
protocol: { json: true, legacy: true, strictSession: false },
},
auth: {
mode: 'public',
Expand Down
11 changes: 3 additions & 8 deletions apps/e2e/demo-e2e-multiapp/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,9 @@ const port = parseInt(process.env['PORT'] ?? '3022', 10);
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
},
transport: {
protocol: { json: true, legacy: true, strictSession: false },
},
})
export default class Server {}
11 changes: 3 additions & 8 deletions apps/e2e/demo-e2e-notifications/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ const port = parseInt(process.env['PORT'] ?? '3020', 10);
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
},
transport: {
protocol: { json: true, legacy: true, strictSession: false },
},
})
export default class Server {}
11 changes: 3 additions & 8 deletions apps/e2e/demo-e2e-openapi/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ const port = parseInt(process.env['PORT'] ?? '3012', 10);
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
},
transport: {
protocol: { json: true, legacy: true, strictSession: false },
},
})
export default class Server {}
16 changes: 9 additions & 7 deletions apps/e2e/demo-e2e-openapi/src/security-test-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ class SecurityTestApp {}
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
transport: {
protocol: {
sse: true,
streamable: true,
json: true,
stateless: false,
legacy: true,
strictSession: false,
},
},
})
Expand Down
16 changes: 10 additions & 6 deletions apps/e2e/demo-e2e-orchestrated/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ const port = parseInt(process.env['PORT'] ?? '3005', 10);
requireSelection: true,
rememberConsent: true,
},
sessionMode: 'stateful',
tokenStorage: {
type: 'memory', // Use 'redis' in production
},
allowDefaultPublic: false,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
},
transport: {
sessionMode: 'stateful',
protocol: {
sse: true,
streamable: true,
json: true,
stateless: false,
legacy: false,
strictSession: false,
},
},
})
Expand Down
Loading
Loading