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
8 changes: 4 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ function generateDeviceId(): string {
*/
function ensureConfigDir(): void {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
}
}

Expand Down Expand Up @@ -384,9 +384,9 @@ export function saveConfig(config: ProxyConfig): void {
}
}

// Atomic write: write to tmp, then rename
// Atomic write: write to tmp with restricted permissions, then rename
const data = JSON.stringify(config, null, 2);
fs.writeFileSync(CONFIG_TMP, data);
fs.writeFileSync(CONFIG_TMP, data, { mode: 0o600 });
fs.renameSync(CONFIG_TMP, CONFIG_FILE);
}

Expand Down Expand Up @@ -459,7 +459,7 @@ export function setApiKey(key: string): void {
creds = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
}
creds.apiKey = key;
fs.writeFileSync(credPath, JSON.stringify(creds, null, 2));
fs.writeFileSync(credPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
} catch {}
}

Expand Down
4 changes: 2 additions & 2 deletions src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ export function saveAgentCredentials(creds: Partial<AgentCredentials>): void {
const dir = path.dirname(credPath);

if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}

const existing = loadAgentCredentials();
const merged: AgentCredentials = { ...existing, ...creds };
fs.writeFileSync(credPath, JSON.stringify(merged, null, 2) + '\n');
fs.writeFileSync(credPath, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
}

/**
Expand Down
24 changes: 20 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,12 @@ export class ProxyServer {
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);

// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
// CORS headers — restrict to localhost origins to prevent cross-site data exfiltration
const origin = req.headers.origin as string | undefined;
const allowedOrigins = ['http://localhost:4100', 'http://localhost:3000', 'http://127.0.0.1:4100', 'http://127.0.0.1:3000'];
if (origin && allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-RelayPlane-Workspace, X-RelayPlane-Agent, X-RelayPlane-Session, X-RelayPlane-Automated');

Expand Down Expand Up @@ -1370,13 +1374,25 @@ export class ProxyServer {
);
}

/** Maximum request body size (1 MB) */
private static readonly MAX_BODY_SIZE = 1_048_576;

/**
* Read request body
* Read request body with size limit to prevent memory exhaustion from oversized payloads.
*/
private readBody(req: http.IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk) => (body += chunk));
let size = 0;
req.on('data', (chunk: Buffer | string) => {
size += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
if (size > ProxyServer.MAX_BODY_SIZE) {
req.destroy();
reject(new Error('Request body exceeds 1 MB size limit'));
return;
}
body += chunk;
});
req.on('end', () => resolve(body));
req.on('error', reject);
});
Expand Down