-
Notifications
You must be signed in to change notification settings - Fork 23
fix(W-18094367): remove support for .netrc files #170
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"cSpell.words": [ | ||
"herokurc", | ||
"yubikey" | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
const fs = require('fs') | ||
const os = require('os') | ||
const path = require('path') | ||
/** | ||
* List of Heroku machines to remove from the .netrc file | ||
* as they are no longer used and pose a security risk. | ||
* | ||
* Add any additional Heroku machines to this list that you | ||
* want to remove from the .netrc file. | ||
*/ | ||
const machinesToRemove = ['api.heroku.com', 'git.heroku.com', 'api.staging.herokudev.com'] | ||
/** | ||
* Removes the unencrypted Heroku entries from the .netrc file | ||
* This is a mitigation for a critical security vulnerability | ||
* where unencrypted Heroku API tokens could be leaked | ||
* if the .netrc file is compromised. This function removes | ||
* any entries related to Heroku from the .netrc file as Heroku | ||
* has discontinued it's use. | ||
* | ||
* BE ADVISED: a defect exists in the original implementation | ||
* where orphaned credentials (passwords without machine blocks) | ||
* are created when attempting to delete machine entries using the | ||
* netrc-parser library. | ||
* | ||
* This implementation corrects that issue by removing orphaned | ||
* credentials as well. | ||
* | ||
* @returns {void} | ||
*/ | ||
function removeUnencryptedNetrcMachines() { | ||
try { | ||
const netrcPath = getNetrcFileLocation() | ||
|
||
if (!fs.existsSync(netrcPath)) { | ||
console.log('.netrc file not found, nothing to clean up') | ||
return | ||
} | ||
|
||
const content = fs.readFileSync(netrcPath, 'utf8') | ||
const lines = content.split('\n') | ||
const filteredLines = [] | ||
let skipLines = false | ||
|
||
// Iterate through lines, handling machine blocks and orphaned credentials | ||
for (const line of lines) { | ||
const trimmedLine = line.trim().toLowerCase() | ||
|
||
// Check if we're starting a Heroku machine block | ||
if (trimmedLine.startsWith('machine') && | ||
(machinesToRemove.some(machine => trimmedLine.includes(machine)))) { | ||
skipLines = true | ||
continue | ||
} | ||
|
||
// Check if we're starting a new machine block (non-Heroku) | ||
if (trimmedLine.startsWith('machine') && !skipLines) { | ||
skipLines = false | ||
} | ||
|
||
// Check for orphaned Heroku passwords (HKRU-) and their associated usernames | ||
if (/(HRKUSTG-|HKRU-)/.test(line)) { | ||
// Remove the previous line if it exists (username) | ||
if (filteredLines.length > 0) { | ||
filteredLines.pop() | ||
} | ||
|
||
continue | ||
} | ||
|
||
// Only keep lines if we're not in a Heroku block | ||
if (!skipLines) { | ||
filteredLines.push(line) | ||
} | ||
} | ||
|
||
// Remove any trailing empty lines | ||
while (filteredLines.length > 0 && !filteredLines[filteredLines.length - 1].trim()) { | ||
filteredLines.pop() | ||
} | ||
|
||
// Add a newline at the end if we have content | ||
const outputContent = filteredLines.length > 0 ? | ||
filteredLines.join('\n') + '\n' : | ||
'' | ||
|
||
fs.writeFileSync(netrcPath, outputContent) | ||
} catch (error) { | ||
throw new Error(`Error cleaning up .netrc: ${error.message}`) | ||
} | ||
} | ||
|
||
/** | ||
* Finds the absolute path to the .netrc file | ||
* on disk based on the operating system. This | ||
* code was copied directly from `netrc-parser` | ||
* and optimized for use here. | ||
* | ||
* @see [netrc-parser](https://github.com/jdx/node-netrc-parser/blob/master/src/netrc.ts#L177) | ||
* | ||
* @returns {string} the file path of the .netrc on disk. | ||
*/ | ||
function getNetrcFileLocation() { | ||
let home = '' | ||
if (os.platform() === 'win32') { | ||
home = | ||
process.env.HOME ?? | ||
(process.env.HOMEDRIVE && process.env.HOMEPATH && path.join(process.env.HOMEDRIVE, process.env.HOMEPATH)) ?? | ||
process.env.USERPROFILE | ||
} | ||
|
||
if (!home) { | ||
home = os.homedir() ?? os.tmpdir() | ||
} | ||
|
||
return path.join(home, os.platform() === 'win32' ? '_netrc' : '.netrc') | ||
} | ||
|
||
removeUnencryptedNetrcMachines() | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import {Interfaces} from '@oclif/core' | ||
import {CLIError, warn} from '@oclif/core/lib/errors' | ||
import {HTTP, HTTPError, HTTPRequestOptions} from '@heroku/http-call' | ||
import Netrc from 'netrc-parser' | ||
import * as url from 'url' | ||
|
||
import deps from './deps' | ||
|
@@ -11,7 +10,11 @@ | |
import {vars} from './vars' | ||
import {ParticleboardClient, IDelinquencyInfo, IDelinquencyConfig} from './particleboard-client' | ||
|
||
const debug = require('debug') | ||
import debug from 'debug' | ||
import {removeToken, retrieveToken} from './token-storage' | ||
|
||
// intentional side effect | ||
import '../scripts/cleanup-netrc.cjs' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as prior, that activation/calls to the clean-up code should be in a separate subsequent CLI release, after the major release implementing the new storage mechanism. |
||
|
||
export namespace APIClient { | ||
export interface Options extends HTTPRequestOptions { | ||
|
@@ -70,7 +73,7 @@ | |
if (options.debug) debug.enable('http') | ||
if (options.debug && options.debugHeaders) debug.enable('http,http:headers') | ||
this.options = options | ||
const apiUrl = url.URL ? new url.URL(vars.apiUrl) : url.parse(vars.apiUrl) | ||
Check warning on line 76 in src/api-client.ts
|
||
const envHeaders = JSON.parse(process.env.HEROKU_HEADERS || '{}') | ||
this.preauthPromises = {} | ||
const self = this as any | ||
|
@@ -192,7 +195,7 @@ | |
} | ||
|
||
if (!Object.keys(opts.headers).some(h => h.toLowerCase() === 'authorization')) { | ||
opts.headers.authorization = `Bearer ${self.auth}` | ||
opts.headers.authorization = `Bearer ${await self.auth}` | ||
} | ||
|
||
this.configDelinquency(url, opts) | ||
|
@@ -204,7 +207,7 @@ | |
const particleboardClient: ParticleboardClient = self.particleboard | ||
|
||
if (delinquencyConfig.fetch_delinquency && !delinquencyConfig.warning_shown) { | ||
self._particleboard.auth = self.auth | ||
self._particleboard.auth = await self.auth | ||
const settledResponses = await Promise.allSettled([ | ||
super.request<T>(url, opts), | ||
particleboardClient.get<IDelinquencyInfo>(delinquencyConfig.fetch_url as string), | ||
|
@@ -240,7 +243,7 @@ | |
|
||
if (!self.authPromise) self.authPromise = self.login() | ||
await self.authPromise | ||
opts.headers.authorization = `Bearer ${self.auth}` | ||
opts.headers.authorization = `Bearer ${await self.auth}` | ||
return this.request<T>(url, opts, retries) | ||
} | ||
|
||
|
@@ -273,9 +276,13 @@ | |
if (!this._auth) { | ||
if (process.env.HEROKU_API_TOKEN && !process.env.HEROKU_API_KEY) deps.cli.warn('HEROKU_API_TOKEN is set but you probably meant HEROKU_API_KEY') | ||
this._auth = process.env.HEROKU_API_KEY | ||
if (!this._auth) { | ||
deps.netrc.loadSync() | ||
this._auth = deps.netrc.machines[vars.apiHost] && deps.netrc.machines[vars.apiHost].password | ||
} | ||
|
||
if (!this._auth) { | ||
try { | ||
this._auth = retrieveToken() | ||
} catch { | ||
// noop | ||
} | ||
} | ||
|
||
|
@@ -346,9 +353,7 @@ | |
if (error instanceof CLIError) warn(error) | ||
} | ||
|
||
delete Netrc.machines['api.heroku.com'] | ||
delete Netrc.machines['git.heroku.com'] | ||
await Netrc.save() | ||
removeToken() | ||
} | ||
|
||
get defaults(): typeof HTTP.defaults { | ||
|
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.
I think it's fine to include the code for deleting the old cleartext Netrc entries, but I do not agree that it should be automatically run on the initial release of the new storage mechanism. Let's put the activation/calls to
removeUnencryptedNetrcMachines()
in a subsequent PR that can be released in a patch version after the major release.If initial release of the new storage mechanism breaks something for a customer, they need to be able to CLI update back to the previous CLI version to restore functionality.