diff --git a/AI/mcp-server/package.json b/AI/mcp-server/package.json index f305309..a8a15a9 100644 --- a/AI/mcp-server/package.json +++ b/AI/mcp-server/package.json @@ -12,6 +12,7 @@ "@azure/msal-node": "^5.4.1", "@modelcontextprotocol/sdk": "^1.29.0", "express": "^5.2.1", + "express-rate-limit": "^8.6.0", "zod": "^4.4.3" }, "engines": { diff --git a/AI/mcp-server/src/server.ts b/AI/mcp-server/src/server.ts index 9b320c4..a7c3088 100644 --- a/AI/mcp-server/src/server.ts +++ b/AI/mcp-server/src/server.ts @@ -3,6 +3,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import express, { type Request, type Response, type NextFunction } from 'express'; import { randomUUID } from 'node:crypto'; +import rateLimit from 'express-rate-limit'; import type { GraphClient } from './graph.js'; import type { AppConfig } from './config.js'; import { registerContainerTools } from './tools/containers.js'; @@ -80,6 +81,14 @@ export function buildApp(graph: GraphClient, config: AppConfig): express.Applica next(); } + // ── Rate limiting ───────────────────────────────────────────────────────────── + const messagesLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + standardHeaders: true, + legacyHeaders: false, + }); + // ── Health ──────────────────────────────────────────────────────────────────── app.get('/health', (_req, res) => { res.json({ status: 'ok', service: 'spe-mcp-server', version: '1.0.0' }); @@ -251,7 +260,7 @@ export function buildApp(graph: GraphClient, config: AppConfig): express.Applica } }); - app.post('/messages', requireBearerToken, async (req: Request, res: Response) => { + app.post('/messages', messagesLimiter, requireBearerToken, async (req: Request, res: Response) => { const sessionId = req.query['sessionId'] as string | undefined; if (!sessionId) { res.status(400).json({ error: 'Missing sessionId' }); return; } const transport = sseTransports.get(sessionId); diff --git a/AI/ocr/server/index.ts b/AI/ocr/server/index.ts index b13df75..906df47 100644 --- a/AI/ocr/server/index.ts +++ b/AI/ocr/server/index.ts @@ -5,13 +5,22 @@ import { onReceiptAdded } from "./onReceiptAdded"; const app = express(); +// Allowed origins: configure via ALLOWED_ORIGINS env var (comma-separated list). +// Defaults to common local development origins. +const allowedOrigins = new Set( + (process.env.ALLOWED_ORIGINS || 'http://localhost:3000,http://localhost:3001,https://localhost:3000,https://localhost:3001').split(',').map(o => o.trim()).filter(Boolean) +); + app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use((req: Request, res: Response, next: NextFunction) => { - res.header('Access-Control-Allow-Origin', req.header('origin')); - res.header('Access-Control-Allow-Headers', req.header('Access-Control-Request-Headers')); - res.header('Access-Control-Allow-Credentials', 'true'); + const origin = req.header('origin'); + if (origin && allowedOrigins.has(origin)) { + res.header('Access-Control-Allow-Origin', origin); + res.header('Access-Control-Allow-Headers', req.header('Access-Control-Request-Headers')); + res.header('Access-Control-Allow-Credentials', 'true'); + } if (req.method === 'OPTIONS') { return res.sendStatus(204); diff --git a/Custom Apps/boilerplate-aspnet-webservice/Services/MSGraphService.cs b/Custom Apps/boilerplate-aspnet-webservice/Services/MSGraphService.cs index 32e2307..ee7a5d5 100644 --- a/Custom Apps/boilerplate-aspnet-webservice/Services/MSGraphService.cs +++ b/Custom Apps/boilerplate-aspnet-webservice/Services/MSGraphService.cs @@ -163,7 +163,7 @@ public async Task> GetContainerPermissions public async Task UpdateContainerPermission(string accessToken, string containerId, string permissionId, string role) { HttpClient client = GetHttpClient(accessToken, "application/json"); - var json = $@"{{ ""roles"":[""{role}""]}}"; + var json = JsonConvert.SerializeObject(new { roles = new[] { role } }); HttpContent content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await client.PatchAsync($"{GraphContainersEndpoint}/{containerId}/permissions/{permissionId}", content); diff --git a/Custom Apps/boilerplate-react-azurefunction/packages/azure-functions/web.config b/Custom Apps/boilerplate-react-azurefunction/packages/azure-functions/web.config index 3c21bfc..2603003 100644 --- a/Custom Apps/boilerplate-react-azurefunction/packages/azure-functions/web.config +++ b/Custom Apps/boilerplate-react-azurefunction/packages/azure-functions/web.config @@ -1,6 +1,11 @@ + + + + + diff --git a/Custom Apps/webhook/src/server.js b/Custom Apps/webhook/src/server.js index ead69e4..273d82f 100644 --- a/Custom Apps/webhook/src/server.js +++ b/Custom Apps/webhook/src/server.js @@ -31,6 +31,13 @@ app.post('/webhook', async (req, res) => { const resource = notification.resource; // e.g., drives/{drive-id}/items/{item-id} if (graphToken) { + // Validate the resource path to prevent path traversal or unexpected endpoints. + // Only allow alphanumeric characters, hyphens, underscores, and forward slashes. + const safeResourcePattern = /^[A-Za-z0-9_\-/]+$/; + if (!resource || !safeResourcePattern.test(resource)) { + console.warn('⚠️ Skipping metadata fetch — invalid resource path:', resource); + continue; + } try { const response = await axios.get(`https://graph.microsoft.com/v1.0/${resource}`, { headers: {