Root cause
Injection & unsafe execution introduced
feat: Auto-update .env file with refreshed tokens
How AI contributed: Direct introductionPublished Feb 19, 2026 in
yosefhayim/ebay-mcp
The ebay_set_user_tokens tool allows updating the .env file with new tokens. The updateEnvFile function in src/auth/oauth.ts blindly appends or replaces values without validating them for newlines or quotes. This allows an attacker to inject arbitrary environment variables into the configuration file. Impact An attacker can inject arbitrary environment variables into the .env file. This could lead to: · Configuration Overwrites: Attackers can overwrite critical settings like EBAY_REDIRECT_URI to hijack OAuth flows. · Denial of Service: Injecting invalid configuration can prevent the server from starting. · Potential RCE: In some environments, controlling environment variables (like NODE_OPTIONS) can lead to Remote Code Execution. Found with MCPwner 🕶
Claude author 4c9c826c adds updateEnvFile with raw ${key}=${value} writeback and every token persistence call. Parent a00b6c59 has no updateEnvFile. Fix aab0bda7 uses dotenv.parse + dotenv-stringify. v1.7.2 contains origin without fix; v1.7.3 contains the fix.
Root cause
feat: Auto-update .env file with refreshed tokens
Fix
Replaces regex-based raw .env line construction with dotenv.parse plus dotenv-stringify before writing refreshed tokens, so token contents are encoded by the env serializer instead of becoming new assignments.
Case facts
@@ -7,6 +7,39 @@ import type { StoredTokenData, } from '@/types/ebay.js'; import { LocaleEnum } from '@/types/ebay-enums.js';+import { readFileSync, writeFileSync } from 'fs';+import { join } from 'path';++/**+ * Update .env file with new token values+ */+function updateEnvFile(updates: { [key: string]: string }): void {+ try {+ const envPath = join(process.cwd(), '.env');+ let envContent = readFileSync(envPath, 'utf-8');++ // Update each key-value pair+ for (const [key, value] of Object.entries(updates)) {+ // Match the key with or without value, handling comments+ const regex = new RegExp(`^(#\\s*)?${key}=.*$`, 'gm');+ const newLine = `${key}=${value}`;++ if (regex.test(envContent)) {+ // Update existing key (uncomment if needed)+ envContent = envContent.replace(regex, newLine);+ } else {+ // Add new key at the end+ envContent += `\n${newLine}`;+ }+ }++ writeFileSync(envPath, envContent, 'utf-8');+ console.log('✅ Updated .env file with new tokens');+ } catch (error) {+ console.error('⚠️ Failed to update .env file:', error instanceof Error ? error.message : error);+ console.error(' Please manually update your .env file with the new tokens');+ }+} /** * Manages eBay OAuth 2.0 authentication@@ -137,9 +170,8 @@ export class EbayOAuthClient { } /**- * Set user access token and refresh token in memory- * Note: Tokens are loaded from .env and stored in memory only- * To persist tokens, update the .env file with EBAY_USER_REFRESH_TOKEN+ * Set user access token and refresh token+ * Stores tokens in memory and updates .env file for persistence */ setUserTokens( accessToken: string,@@ -159,6 +191,12 @@ export class EbayOAuthClient { userAccessTokenExpiry: now + 7200 * 1000, // Default 2 hours userRefreshTokenExpiry: now + 18 * 30 * 24 * 60 * 60 * 1000, // Default 18 months };++ // Update .env file with new tokens+ updateEnvFile({+ EBAY_USER_ACCESS_TOKEN: accessToken,+ EBAY_USER_REFRESH_TOKEN: refreshToken,+ }); } /**@@ -200,6 +238,11 @@ export class EbayOAuthClient { // Set expiry with 60 second buffer this.appAccessTokenExpiry = Date.now() + (response.data.expires_in - 60) * 1000; + // Update .env file with app access token+ updateEnvFile({+ EBAY_APP_ACCESS_TOKEN: this.appAccessToken,+ });+ return this.appAccessToken; } catch (error) { if (axios.isAxiosError(error)) {@@ -323,15 +366,19 @@ export class EbayOAuthClient { scope: tokenData.scope || this.userTokens.scope, }; - // If eBay provided a new refresh token, inform user to update .env- if (- tokenData.refresh_token &&- tokenData.refresh_token !== process.env.EBAY_USER_REFRESH_TOKEN- ) {- console.error('\neBay issued a new refresh token!');- console.error('Please update your .env file with:');- console.error(`EBAY_USER_REFRESH_TOKEN="${tokenData.refresh_token}"\n`);+ // Update .env file with new tokens@@ -7,6 +7,8 @@ import type { StoredTokenData, } from '@/types/ebay.js'; import { LocaleEnum } from '@/types/ebay-enums.js';+import dotenv from 'dotenv';+import stringify from 'dotenv-stringify'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { authLogger } from '@/utils/logger.js';@@ -17,31 +19,23 @@ import { authLogger } from '@/utils/logger.js'; function updateEnvFile(updates: Record<string, string>): void { try { const envPath = join(process.cwd(), '.env');- let envContent = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';+ const existingEnv = existsSync(envPath) ? dotenv.parse(readFileSync(envPath, 'utf-8')) : {}; - // Update each key-value pair- for (const [key, value] of Object.entries(updates)) {- // Match the key with or without value, handling comments- const regex = new RegExp(`^(#\\s*)?${key}=.*$`, 'gm');- const newLine = `${key}="${value}"`;+ // Merge new updates into existing environment object+ const mergedEnv = { ...existingEnv, ...updates }; - if (regex.test(envContent)) {- // Update existing key (uncomment if needed)- envContent = envContent.replace(regex, newLine);- } else {- // Add new key at the end- envContent += `\n${newLine}`;- }- }+ // Securely stringify the merged environment object+ const safeEnvContent = stringify(mergedEnv); - writeFileSync(envPath, envContent, 'utf-8');- // Tokens updated silently - console output interferes with MCP JSON protocol+ // Write the updated content back to the .env file+ writeFileSync(envPath, safeEnvContent, 'utf-8'); } catch (_error) { // Silent failure - error logging interferes with MCP JSON protocol // If needed, check .env file manually } } + /** * Manages eBay OAuth 2.0 authentication * Loads tokens exclusively from environment variables (.env file)AI-assisted change 955c6c5c6e3ecaa0b2c90dd57f5e74948d1b9b2595e72aefa89136b1f7642754 · Fix 17d45ffd26a944edd7d4da82e9cfecf932cc335132f80e2b64198109455bc4cb