-
Notifications
You must be signed in to change notification settings - Fork 2
Implement security enhancements in auth-client: validate base URL, cl… #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…ient ID, and token structure. Improve error handling and streamline token management by utilizing SecurityManager methods.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
cursor-link-cli/src/utils/auth-client.ts (2)
42-47: Set secure permissions on config directory (0700)Prevents other users from listing token files.
private ensureConfigDir() { if (!fs.existsSync(CONFIG_DIR)) { - fs.mkdirSync(CONFIG_DIR, { recursive: true }); + fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); + } else { + try { fs.chmodSync(CONFIG_DIR, 0o700); } catch {} } }
76-79: Persist tokens with 0600 perms and atomicallyAvoid world-readable 0644 via umask defaults; use atomic rename.
saveToken(tokenData: StoredToken) { this.ensureConfigDir(); - fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokenData, null, 2)); + const tmp = path.join(CONFIG_DIR, 'token.json.tmp'); + const payload = JSON.stringify(tokenData, null, 2); + fs.writeFileSync(tmp, payload, { mode: 0o600 }); + fs.renameSync(tmp, TOKEN_FILE); }
🧹 Nitpick comments (13)
cursor-link-cli/src/utils/security.ts (4)
41-52: Make token clearing more resilientIf chmod/write fails (permissions/AV), fall back to rm -f. Also prefer rmSync over unlinkSync.
if (fs.existsSync(TOKEN_FILE)) { - // Overwrite with zeros before deletion (basic security) + // Overwrite with zeros before deletion (best-effort) const stats = fs.statSync(TOKEN_FILE); const zeros = Buffer.alloc(stats.size, 0); - fs.writeFileSync(TOKEN_FILE, zeros); - fs.unlinkSync(TOKEN_FILE); + try { fs.chmodSync(TOKEN_FILE, 0o600); } catch {} + try { fs.writeFileSync(TOKEN_FILE, zeros); } catch {} + fs.rmSync(TOKEN_FILE, { force: true }); } } catch (error) { - console.warn('Warning: Could not securely clear token file:', error); + console.warn('Warning: Could not securely clear token file:', error); + try { fs.rmSync(TOKEN_FILE, { force: true }); } catch {} }
57-61: Client ID regex: avoid leading/trailing hyphen; consider underscores/dots if server allowsCurrent pattern allows "-" at ends and disallows "" or ".". Tighten ends and optionally allow [.].
- const clientIdPattern = /^[a-zA-Z0-9-]{3,50}$/; + // Starts/ends with alnum; internal [-._] allowed. Adjust if server forbids _ or . + const clientIdPattern = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{1,48})[A-Za-z0-9]$/;Please confirm allowed characters on the auth server before merging.
91-104: Nit: prefer error_description over terse error codeShow human-friendly descriptions first.
- const safeFields = ['error', 'error_description', 'message']; + const safeFields = ['error_description', 'message', 'error'];
5-7: Avoid duplicating token path constants across modulesCONFIG_DIR/TOKEN_FILE are defined here and in auth-client.ts. Extract to a shared const/module to prevent drift.
cursor-link-cli/src/utils/auth-client.ts (6)
70-72: Self-heal on corrupt token fileIf JSON.parse fails, clear the token to avoid repeated failures.
- const sanitizedError = SecurityManager.sanitizeError(error); - console.error(`Error reading stored token: ${sanitizedError}`); + const sanitizedError = SecurityManager.sanitizeError(error); + console.error(`Error reading stored token: ${sanitizedError}`); + this.clearToken(); return null;
121-129: Allow env override for client IDKeeps defaults but enables testing/alternate clients.
- const clientId = "cursor-link-cli"; + const clientId = process.env.CURSOR_LINK_CLIENT_ID || "cursor-link-cli";
151-157: Prefer verification_uri_complete when present; only prompt for code if neededImproves UX and matches OAuth device flow guidance.
- console.log(chalk.cyan('\n📱 Device Authorization in Progress')); - console.log(chalk.white(`Please visit: ${verification_uri}`)); - console.log(chalk.white(`Enter code: ${chalk.bold.yellow(user_code)}\n`)); + console.log(chalk.cyan('\n📱 Device Authorization in Progress')); + const displayUrl = verification_uri_complete || verification_uri; + console.log(chalk.white(`Please visit: ${displayUrl}`)); + if (!verification_uri_complete) { + console.log(chalk.white(`Enter code: ${chalk.bold.yellow(user_code)}\n`)); + } else { + console.log(); + }
156-164: Guard browser open against non-http(s) URLsBe defensive with untrusted inputs.
- const urlToOpen = verification_uri_complete || verification_uri; + const urlToOpen = verification_uri_complete || verification_uri; if (urlToOpen) { - console.log(chalk.gray('🌐 Opening browser...')); - try { - await open(urlToOpen); + console.log(chalk.gray('🌐 Opening browser...')); + try { + if (/^https?:\/\//i.test(urlToOpen)) { + await open(urlToOpen); + } } catch (error) { console.log(chalk.gray('Failed to open browser automatically. Please visit the URL manually.')); } }
267-271: Nit: avoid printing “undefined” when user fields are missingFallback gracefully to email or id.
- if (token.user) { - console.log(chalk.gray(` User: ${token.user.name} (${token.user.email})`)); - } + if (token.user) { + const userLabel = token.user.name || token.user.email || token.user.id; + console.log(chalk.gray(` User: ${userLabel}`)); + }
12-14: De-duplicate token path constantsCONFIG_DIR/TOKEN_FILE are also defined in SecurityManager. Extract to a shared module (e.g., utils/paths.ts) and import from both to prevent drift.
cursor-link-cli/SECURITY.md (3)
50-57: Reflect URL validation hardening in docsMention that embedded credentials are blocked and ::1 is allowed in dev.
-// ✅ http://localhost:3000 - allowed in dev +// ✅ http://localhost:3000 - allowed in dev +// ✅ http://[::1]:3000 - allowed in dev // ❌ http://malicious-site.com - blocked +// ❌ https://user:pass@cursor.link - blocked (credentials in URL)
102-107: Document secure file/dir permissionsClarify 0600/0700 to reduce leakage on multi-user systems.
-**Permissions**: File created with default user permissions +**Permissions**: Directory 0700; token file 0600
95-117: Nit: minor Markdown/grammar tweaks for readabilitySmall fixes: remove double spaces for line breaks, standardize bullet punctuation, and tweak phrasing.
-### 1. Transport Security -- **Production**: Requires HTTPS endpoints -- **Development**: Allows localhost with HTTP -- **Protection**: Prevents man-in-the-middle attacks +### 1. Transport Security +- **Production**: Require HTTPS endpoints. +- **Development**: Allow localhost with HTTP. +- **Protection**: Prevent man-in-the-middle attacks.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
cursor-link-cli/SECURITY.md(1 hunks)cursor-link-cli/src/utils/auth-client.ts(7 hunks)cursor-link-cli/src/utils/security.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/better-auth-device-authorization.mdc)
When calling device.token, set grant_type to "urn:ietf:params:oauth:grant-type:device_code"
Files:
cursor-link-cli/src/utils/security.tscursor-link-cli/src/utils/auth-client.ts
🧠 Learnings (2)
📚 Learning: 2025-09-01T20:24:00.229Z
Learnt from: CR
PR: R44VC0RP/cursor.link#0
File: .cursor/rules/better-auth-device-authorization.mdc:0-0
Timestamp: 2025-09-01T20:24:00.229Z
Learning: Applies to cli-auth.ts : In polling logic, handle OAuth device flow errors: continue on authorization_pending, increase interval on slow_down, and terminate on access_denied or expired_token
Applied to files:
cursor-link-cli/src/utils/auth-client.ts
📚 Learning: 2025-09-01T20:24:00.229Z
Learnt from: CR
PR: R44VC0RP/cursor.link#0
File: .cursor/rules/better-auth-device-authorization.mdc:0-0
Timestamp: 2025-09-01T20:24:00.229Z
Learning: Applies to **/*.{ts,tsx} : When calling device.token, set grant_type to "urn:ietf:params:oauth:grant-type:device_code"
Applied to files:
cursor-link-cli/src/utils/auth-client.ts
🧬 Code graph analysis (1)
cursor-link-cli/src/utils/auth-client.ts (1)
cursor-link-cli/src/utils/security.ts (1)
SecurityManager(12-105)
🪛 LanguageTool
cursor-link-cli/SECURITY.md
[grammar] ~46-~46: There might be a mistake here.
Context: ...}); ``` ### 🔒 Security Enhancements Our implementation adds these securi...
(QB_NEW_EN)
[grammar] ~88-~88: There might be a mistake here.
Context: ...vice Authorization Request (Section 3.1) - ✅ Device Authorization Response (Section...
(QB_NEW_EN)
[grammar] ~89-~89: There might be a mistake here.
Context: ...ice Authorization Response (Section 3.2) - ✅ User Interaction (Section 3.3) - ✅ Dev...
(QB_NEW_EN)
[grammar] ~90-~90: There might be a mistake here.
Context: ...3.2) - ✅ User Interaction (Section 3.3) - ✅ Device Access Token Request (Section 3...
(QB_NEW_EN)
[grammar] ~91-~91: There might be a mistake here.
Context: ...evice Access Token Request (Section 3.4) - ✅ Device Access Token Response (Section ...
(QB_NEW_EN)
[grammar] ~92-~92: There might be a mistake here.
Context: ...vice Access Token Response (Section 3.5) - ✅ Error Response Handling (Section 3.5.2...
(QB_NEW_EN)
[grammar] ~97-~97: There might be a mistake here.
Context: ...est Practices ### 1. Transport Security - Production: Requires HTTPS endpoints -...
(QB_NEW_EN)
[grammar] ~102-102: There might be a mistake here./.cursor-l...
Context: ...-middle attacks ### 2. Token Management - Storage: Tokens stored in `
(QB_NEW_EN)
[grammar] ~103-~103: There might be a mistake here.
Context: ...agement - Storage: Tokens stored in ~/.cursor-link/token.json - Permissions: File created with default...
(QB_NEW_EN)
[grammar] ~104-~104: There might be a mistake here.
Context: ...le created with default user permissions - Expiration: Tokens validated for expir...
(QB_NEW_EN)
[grammar] ~105-~105: There might be a mistake here.
Context: ...validated for expiration with 60s buffer - Cleanup: Secure deletion overwrites da...
(QB_NEW_EN)
[grammar] ~108-~108: There might be a mistake here.
Context: ...fore file removal ### 3. Error Handling - Sanitization: Error messages filtered ...
(QB_NEW_EN)
[grammar] ~113-~113: There might be a mistake here.
Context: ...tive data logged ### 4. Client Security - ID Validation: Client IDs must match s...
(QB_NEW_EN)
[grammar] ~123-~123: There might be a mistake here.
Context: ...HTTPS-only 2. Base URL: Defaults to https://cursor.link 3. Client ID: Uses cursor-link-cli (mus...
(QB_NEW_EN)
[grammar] ~154-~154: There might be a mistake here.
Context: ...Man-in-the-Middle: HTTPS enforcement 2. Token Theft: Secure file permissions &...
(QB_NEW_EN)
[grammar] ~155-~155: There might be a mistake here.
Context: ...eft**: Secure file permissions & cleanup 3. Phishing: URL validation prevents mali...
(QB_NEW_EN)
[grammar] ~156-~156: There might be a mistake here.
Context: ... validation prevents malicious redirects 4. Info Disclosure: Error message sanitiz...
(QB_NEW_EN)
[grammar] ~157-~157: There might be a mistake here.
Context: ...Disclosure**: Error message sanitization 5. Client Impersonation: Client ID valida...
(QB_NEW_EN)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Vade Review
🔇 Additional comments (3)
cursor-link-cli/src/utils/auth-client.ts (2)
18-23: Base URL validation at startup: LGTMGood early exit with clear guidance.
185-188: Grant type is correctly set per guidelinesgrant_type uses urn:ietf:params:oauth:grant-type:device_code. Compliant.
cursor-link-cli/SECURITY.md (1)
20-28: Docs: grant_type snippet matches our guidelineThe example correctly sets grant_type to urn:ietf:params:oauth:grant-type:device_code.
| const requiredFields = ['access_token', 'expires_at']; | ||
| return requiredFields.every(field => | ||
| token.hasOwnProperty(field) && token[field] != null | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: malformed expires_at can bypass expiry checks
validateToken only checks presence, so a non-numeric expires_at (e.g., "oops") passes validation. isTokenExpired will then compute Date.now() > ("oops" - 60000) ⇒ false, treating an invalid token as valid. Enforce types.
Apply:
static validateToken(token: any): boolean {
- if (!token || typeof token !== 'object') return false;
-
- // Required fields for a valid token
- const requiredFields = ['access_token', 'expires_at'];
- return requiredFields.every(field =>
- token.hasOwnProperty(field) && token[field] != null
- );
+ if (!token || typeof token !== 'object') return false;
+ const has = (o: any, k: string) => Object.prototype.hasOwnProperty.call(o, k);
+ if (!has(token, 'access_token') || !has(token, 'expires_at')) return false;
+ if (typeof token.access_token !== 'string' || token.access_token.trim().length === 0) return false;
+ if (typeof token.expires_at !== 'number' || !Number.isFinite(token.expires_at)) return false;
+ return true;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const requiredFields = ['access_token', 'expires_at']; | |
| return requiredFields.every(field => | |
| token.hasOwnProperty(field) && token[field] != null | |
| ); | |
| static validateToken(token: any): boolean { | |
| if (!token || typeof token !== 'object') return false; | |
| const has = (o: any, k: string) => Object.prototype.hasOwnProperty.call(o, k); | |
| if (!has(token, 'access_token') || !has(token, 'expires_at')) return false; | |
| if (typeof token.access_token !== 'string' || token.access_token.trim().length === 0) return false; | |
| if (typeof token.expires_at !== 'number' || !Number.isFinite(token.expires_at)) return false; | |
| return true; | |
| } |
🤖 Prompt for AI Agents
In cursor-link-cli/src/utils/security.ts around lines 20-23, validateToken
currently only checks for presence and allows non-numeric expires_at values;
update validateToken to require access_token be a non-empty string and
expires_at be a finite number (or a string that parses to an integer) — reject
if Number.isFinite(Number(token.expires_at)) is false or access_token is not a
string/empty. Also harden isTokenExpired to treat invalid or unparsable
expires_at as expired (return true) before doing the Date.now() comparison.
| static isTokenExpired(token: any): boolean { | ||
| if (!token.expires_at) return true; | ||
|
|
||
| // Add 60 second buffer to avoid edge cases | ||
| const bufferTime = 60 * 1000; | ||
| return Date.now() > (token.expires_at - bufferTime); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: guard isTokenExpired against non-numeric expires_at
Add strict type/finite checks so invalid tokens are treated as expired.
static isTokenExpired(token: any): boolean {
- if (!token.expires_at) return true;
-
- // Add 60 second buffer to avoid edge cases
- const bufferTime = 60 * 1000;
- return Date.now() > (token.expires_at - bufferTime);
+ if (typeof token?.expires_at !== 'number' || !Number.isFinite(token.expires_at)) {
+ return true;
+ }
+ const bufferTime = 60 * 1000;
+ return Date.now() > (token.expires_at - bufferTime);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| static isTokenExpired(token: any): boolean { | |
| if (!token.expires_at) return true; | |
| // Add 60 second buffer to avoid edge cases | |
| const bufferTime = 60 * 1000; | |
| return Date.now() > (token.expires_at - bufferTime); | |
| } | |
| static isTokenExpired(token: any): boolean { | |
| if (typeof token?.expires_at !== 'number' || !Number.isFinite(token.expires_at)) { | |
| return true; | |
| } | |
| const bufferTime = 60 * 1000; | |
| return Date.now() > (token.expires_at - bufferTime); | |
| } |
🤖 Prompt for AI Agents
In cursor-link-cli/src/utils/security.ts around lines 29 to 35, the
isTokenExpired function should treat tokens with invalid or non-numeric
expires_at as expired; add a guard that checks typeof token.expires_at ===
'number' and Number.isFinite(token.expires_at) (or, if you want to accept
numeric strings, attempt Number(token.expires_at) and validate with
Number.isFinite) and return true immediately when the check fails, otherwise
proceed with the existing buffer calculation and comparison against Date.now().
| static validateBaseUrl(baseUrl: string): boolean { | ||
| try { | ||
| const url = new URL(baseUrl); | ||
|
|
||
| // In production, require HTTPS | ||
| if (process.env.NODE_ENV === 'production') { | ||
| return url.protocol === 'https:'; | ||
| } | ||
|
|
||
| // In development, allow localhost with HTTP | ||
| if (url.protocol === 'http:' && | ||
| (url.hostname === 'localhost' || url.hostname === '127.0.0.1')) { | ||
| return true; | ||
| } | ||
|
|
||
| // Otherwise require HTTPS | ||
| return url.protocol === 'https:'; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Harden URL validation: forbid embedded credentials; allow IPv6 loopback
Prevent auth endpoints like https://user:pass@host, and support ::1 in development.
static validateBaseUrl(baseUrl: string): boolean {
try {
const url = new URL(baseUrl);
+ // Disallow embedded credentials
+ if (url.username || url.password) return false;
+
// In production, require HTTPS
if (process.env.NODE_ENV === 'production') {
return url.protocol === 'https:';
}
// In development, allow localhost with HTTP
- if (url.protocol === 'http:' &&
- (url.hostname === 'localhost' || url.hostname === '127.0.0.1')) {
+ if (
+ url.protocol === 'http:' &&
+ (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1')
+ ) {
return true;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| static validateBaseUrl(baseUrl: string): boolean { | |
| try { | |
| const url = new URL(baseUrl); | |
| // In production, require HTTPS | |
| if (process.env.NODE_ENV === 'production') { | |
| return url.protocol === 'https:'; | |
| } | |
| // In development, allow localhost with HTTP | |
| if (url.protocol === 'http:' && | |
| (url.hostname === 'localhost' || url.hostname === '127.0.0.1')) { | |
| return true; | |
| } | |
| // Otherwise require HTTPS | |
| return url.protocol === 'https:'; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| static validateBaseUrl(baseUrl: string): boolean { | |
| try { | |
| const url = new URL(baseUrl); | |
| // Disallow embedded credentials | |
| if (url.username || url.password) return false; | |
| // In production, require HTTPS | |
| if (process.env.NODE_ENV === 'production') { | |
| return url.protocol === 'https:'; | |
| } | |
| // In development, allow localhost with HTTP | |
| if ( | |
| url.protocol === 'http:' && | |
| (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1') | |
| ) { | |
| return true; | |
| } | |
| // Otherwise require HTTPS | |
| return url.protocol === 'https:'; | |
| } catch { | |
| return false; | |
| } | |
| } |
🤖 Prompt for AI Agents
In cursor-link-cli/src/utils/security.ts around lines 66-86, the URL validation
must reject URLs that embed credentials and should treat IPv6 loopback as a
valid dev host; update validateBaseUrl to return false when url.username or
url.password are non-empty (to forbid user:pass@host), and in the development
branch allow HTTP if url.hostname is 'localhost', '127.0.0.1', or '::1'; keep
the production branch requiring url.protocol === 'https:' and preserve the
try/catch returning false on parse errors.
…ient ID, and token structure. Improve error handling and streamline token management by utilizing SecurityManager methods.
Summary by CodeRabbit
New Features
Security
Documentation