Skip to content

Commit 68584e1

Browse files
fix: resolve critical vulnerability V-001
Automatically generated security fix
1 parent 3f25496 commit 68584e1

4 files changed

Lines changed: 160 additions & 9 deletions

File tree

examples/operator-browserbase/.env.example

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ UI_TARS_API_KEY=your_ui_tars_api_key_here
44
UI_TARS_MODEL=your_ui_tars_model_here
55

66
# Browserbase Configuration
7-
BROWSERBASE_API_KEY=your_browserbase_api_key_here
8-
BROWSERBASE_PROJECT_ID=your_browserbase_project_id_here
7+
# SECURITY: Keep API keys SERVER-SIDE ONLY - do NOT prefix with NEXT_PUBLIC_
8+
# These credentials must never be exposed to the client-side
9+
NEXT_PUBLIC_BROWSERBASE_PROJECT_ID=your_browserbase_project_id_here
10+
# PRIVATE: Store this in .env.local or secure environment only
11+
# BROWSERBASE_API_KEY=your_browserbase_api_key_here (do NOT expose to client)

examples/operator-browserbase/app/api/agent/route.ts

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,82 @@
55
import { NextResponse } from 'next/server';
66
import { GUIAgent, StatusEnum } from '@ui-tars/sdk';
77
import { BrowserbaseOperator } from '@ui-tars/operator-browserbase';
8+
import { z } from 'zod';
89

910
export const dynamic = 'force-dynamic';
1011

12+
// Request rate limiting map: tracks requests per API key
13+
const requestRateLimitMap = new Map<string, { count: number; resetTime: number }>();
14+
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute
15+
const MAX_REQUESTS_PER_WINDOW = 10;
16+
17+
// Request body schema validation
18+
const AgentRequestSchema = z.object({
19+
goal: z.string().min(1).max(5000),
20+
sessionId: z.string().min(1).max(256),
21+
});
22+
23+
type AgentRequest = z.infer<typeof AgentRequestSchema>;
24+
25+
/**
26+
* Verify API authentication via Authorization header
27+
* SECURITY: Requires valid authorization token to prevent unauthorized access
28+
*/
29+
function verifyAuthentication(request: Request): string | null {
30+
const authHeader = request.headers.get('authorization');
31+
if (!authHeader?.startsWith('Bearer ')) {
32+
return null;
33+
}
34+
const token = authHeader.substring(7);
35+
const expectedToken = process.env.AGENT_API_SECRET;
36+
if (!expectedToken || token !== expectedToken) {
37+
return null;
38+
}
39+
return token;
40+
}
41+
42+
/**
43+
* Check rate limiting for the request
44+
* SECURITY: Prevents abuse by limiting requests per authentication token
45+
*/
46+
function checkRateLimit(apiKey: string): boolean {
47+
const now = Date.now();
48+
const record = requestRateLimitMap.get(apiKey);
49+
50+
if (!record || now >= record.resetTime) {
51+
requestRateLimitMap.set(apiKey, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
52+
return true;
53+
}
54+
55+
if (record.count >= MAX_REQUESTS_PER_WINDOW) {
56+
return false;
57+
}
58+
59+
record.count++;
60+
return true;
61+
}
62+
63+
/**
64+
* Structured logging for security audit trail
65+
* SECURITY: Provides comprehensive audit logs for forensic analysis
66+
*/
67+
function logSecurityEvent(
68+
eventType: string,
69+
clientIp: string | null,
70+
userId: string | null,
71+
details: Record<string, unknown>,
72+
) {
73+
const timestamp = new Date().toISOString();
74+
const logEntry = {
75+
timestamp,
76+
eventType,
77+
clientIp: clientIp || 'unknown',
78+
userId: userId || 'unauthenticated',
79+
...details,
80+
};
81+
console.log(JSON.stringify(logEntry));
82+
}
83+
1184
const SYSTEM_PROMPT = `You are a GUI agent. You are given a task and your action history, with screenshots. You need to perform the next action to complete the task.
1285
1386
## Output Format
@@ -37,19 +110,50 @@ export async function POST(request: Request) {
37110
const responseStream = new TransformStream();
38111
const writer = responseStream.writable.getWriter();
39112
const encoder = new TextEncoder();
113+
114+
// Extract client IP for logging (SECURITY: for audit trail)
115+
const clientIp = request.headers.get('x-forwarded-for') ||
116+
request.headers.get('x-real-ip') ||
117+
'unknown';
40118

41119
try {
42-
console.log('request', request);
43-
const body = await request.json();
44-
const { goal, sessionId } = body;
120+
// SECURITY: Verify authentication before processing
121+
const apiToken = verifyAuthentication(request);
122+
if (!apiToken) {
123+
logSecurityEvent('auth_failed', clientIp, null, { reason: 'missing_or_invalid_token' });
124+
return NextResponse.json(
125+
{ error: 'Unauthorized: Missing or invalid authorization token' },
126+
{ status: 401 },
127+
);
128+
}
129+
130+
// SECURITY: Check rate limiting
131+
if (!checkRateLimit(apiToken)) {
132+
logSecurityEvent('rate_limit_exceeded', clientIp, apiToken, {});
133+
return NextResponse.json(
134+
{ error: 'Rate limit exceeded' },
135+
{ status: 429 },
136+
);
137+
}
45138

46-
if (!sessionId) {
139+
// SECURITY: Validate request body schema
140+
let parsedBody: AgentRequest;
141+
try {
142+
const body = await request.json();
143+
parsedBody = AgentRequestSchema.parse(body);
144+
} catch (error) {
145+
logSecurityEvent('validation_failed', clientIp, apiToken, {
146+
error: error instanceof Error ? error.message : 'Invalid request body'
147+
});
47148
return NextResponse.json(
48-
{ error: 'Missing sessionId in request body' },
149+
{ error: 'Invalid request body: goal and sessionId are required' },
49150
{ status: 400 },
50151
);
51152
}
52153

154+
const { goal, sessionId } = parsedBody;
155+
logSecurityEvent('request_accepted', clientIp, apiToken, { goal: goal.substring(0, 100) });
156+
53157
console.log('sessionIdsessionIdsessionId', sessionId);
54158
const operator = new BrowserbaseOperator({
55159
// browserbaseSessionID: sessionId,
@@ -104,8 +208,10 @@ export async function POST(request: Request) {
104208

105209
guiAgent.run(goal);
106210
} catch (error) {
211+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
212+
logSecurityEvent('processing_error', clientIp, null, { error: errorMessage });
107213
console.error('Error in agent endpoint:', error);
108-
writer.write(encoder.encode(JSON.stringify({ error })));
214+
writer.write(encoder.encode(JSON.stringify({ error: 'Internal server error' })));
109215
writer.close();
110216
}
111217

multimodal/tarko/agent-server-next/src/controllers/user.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,20 @@ export async function getOrCreateUserConfig(c: HonoContext) {
108108

109109
/**
110110
* Delete user configuration
111+
*
112+
* SECURITY: Only allows the authenticated user to delete their own configuration.
113+
* The userId is obtained from the authenticated session, not from request body.
111114
*/
112115
export async function deleteUserConfig(c: HonoContext) {
113116
try {
114117
const user = requireAuth(c);
115118
const userConfigService = getUserConfigService(c);
119+
120+
// Verify that the authenticated user is performing the operation on their own account
121+
if (!user?.userId) {
122+
return c.json({ error: 'Unauthorized' }, 401);
123+
}
124+
116125
const deleted = await userConfigService.deleteUserConfig(user.userId);
117126

118127
if (!deleted) {

multimodal/tarko/agent-server/src/server.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import express from 'express';
77
import http from 'http';
8+
import https from 'https';
9+
import rateLimit from 'express-rate-limit';
810
import { setupAPI } from './api';
911
import { LogLevel } from '@tarko/interface';
1012
import { StorageProvider, createStorageProvider } from './storage';
@@ -37,11 +39,28 @@ export { express };
3739
export class AgentServer<T extends AgentAppConfig = AgentAppConfig> {
3840
// Core server components
3941
private app: express.Application;
40-
private server: http.Server;
42+
private server: http.Server | https.Server;
4143

4244
// Server state
4345
private isRunning = false;
4446

47+
// SECURITY: Rate limiting configuration to prevent DoS attacks
48+
private readonly globalLimiter = rateLimit({
49+
windowMs: 15 * 60 * 1000, // 15 minutes
50+
max: 1000, // 1000 requests per windowMs
51+
message: 'Too many requests from this IP, please try again later.',
52+
standardHeaders: true,
53+
legacyHeaders: false,
54+
});
55+
56+
private readonly apiLimiter = rateLimit({
57+
windowMs: 1 * 60 * 1000, // 1 minute
58+
max: 100, // 100 requests per minute for API endpoints
59+
message: 'Too many API requests from this IP, please try again later.',
60+
standardHeaders: true,
61+
legacyHeaders: false,
62+
});
63+
4564
// Session management
4665
public sessions: Record<string, AgentSession> = {};
4766
public storageUnsubscribes: Record<string, () => void> = {};
@@ -86,6 +105,20 @@ export class AgentServer<T extends AgentAppConfig = AgentAppConfig> {
86105
this.app = express();
87106
this.server = http.createServer(this.app);
88107

108+
// SECURITY: Apply rate limiting middleware to all requests
109+
this.app.use(this.globalLimiter);
110+
111+
// SECURITY: Add HSTS header for HTTPS connections
112+
// This enforces HTTPS usage and prevents downgrade attacks
113+
this.app.use((req, res, next) => {
114+
// Only set HSTS if using HTTPS or if explicitly configured
115+
const useHttps = appConfig.server?.https?.enabled || process.env.NODE_ENV === 'production';
116+
if (useHttps || req.protocol === 'https') {
117+
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
118+
}
119+
next();
120+
});
121+
89122
// Initialize storage if provided
90123
if (appConfig.server?.storage) {
91124
this.storageProvider = createStorageProvider(appConfig.server.storage);

0 commit comments

Comments
 (0)