Sorry, the diff of this file is too big to display
+2
-1
@@ -12,3 +12,3 @@ { | ||
| ], | ||
| "version": "4.6.0", | ||
| "version": "4.7.0", | ||
| "author": "Box <oss@box.com>", | ||
@@ -88,2 +88,3 @@ "license": "Apache-2.0", | ||
| "/bin", | ||
| "/LICENSE-THIRD-PARTY.txt", | ||
| "/npm-shrinkwrap.json", | ||
@@ -90,0 +91,0 @@ "/oclif.manifest.json", |
@@ -6,2 +6,3 @@ 'use strict'; | ||
| const _ = require('lodash'); | ||
| const utilities = require('../../../util'); | ||
@@ -26,3 +27,3 @@ class EnvironmentsGetCommand extends BoxCommand { | ||
| } else { | ||
| await this.output(environment); | ||
| await this.output(utilities.maskObjectValuesByKey(environment)); | ||
| } | ||
@@ -36,3 +37,4 @@ } | ||
| EnvironmentsGetCommand.description = 'Get a Box environment'; | ||
| EnvironmentsGetCommand.description = | ||
| 'Get a Box environment or list all configured Box environments.\nclientSecret values are masked in CLI output. To view full secrets, access secure storage directly (for example, macOS Keychain, Windows Credential Manager, or a supported Linux equivalent).'; | ||
@@ -39,0 +41,0 @@ EnvironmentsGetCommand.flags = { |
@@ -70,3 +70,3 @@ 'use strict'; | ||
| await this.updateEnvironments(environmentsObject); | ||
| await this.output(environment); | ||
| await this.output(utilities.maskObjectValuesByKey(environment)); | ||
| } | ||
@@ -73,0 +73,0 @@ } |
+133
-31
@@ -28,3 +28,23 @@ 'use strict'; | ||
| const DEFAULT_ENVIRONMENT_NAME = 'oauth'; | ||
| const OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; | ||
| const LOOPBACK_HOST = 'localhost'; | ||
| const DEFAULT_OPEN_AUTHORIZE_IN_BROWSER = ( | ||
| openFn, | ||
| apps, | ||
| authorizeUrl, | ||
| useIncognito | ||
| ) => { | ||
| if (useIncognito) { | ||
| openFn(authorizeUrl, { | ||
| newInstance: true, | ||
| app: { name: apps.browserPrivate }, | ||
| }); | ||
| } else { | ||
| openFn(authorizeUrl); | ||
| } | ||
| }; | ||
| let oauthCallbackTimeoutMs = OAUTH_CALLBACK_TIMEOUT_MS; | ||
| let openAuthorizeInBrowser = DEFAULT_OPEN_AUTHORIZE_IN_BROWSER; | ||
| async function promptForPlatformAppCredentials(inquirerModule, clientId) { | ||
@@ -115,3 +135,3 @@ if (!clientId) { | ||
| const port = flags.port; | ||
| const redirectUri = `http://localhost:${port}/callback`; | ||
| const redirectUri = `http://${LOOPBACK_HOST}:${port}/callback`; | ||
| const isUnsupportedDefaultAppPort = () => | ||
@@ -162,9 +182,2 @@ useDefaultBoxApp && !SUPPORTED_DEFAULT_APP_PORTS.includes(port); | ||
| if (isUnsupportedDefaultAppPort()) { | ||
| this.info( | ||
| chalk`{red Unsupported port "${port}" for the Official Box CLI app flow. Supported ports: ${SUPPORTED_DEFAULT_APP_PORTS.join(', ')}}` | ||
| ); | ||
| return; | ||
| } | ||
| if (this.flags.reauthorize) { | ||
@@ -211,9 +224,2 @@ // Keep the selected existing environment config for reauthorization. | ||
| if (isUnsupportedDefaultAppPort()) { | ||
| this.info( | ||
| chalk`{red Unsupported port "${port}" for the Official Box CLI app flow. Supported ports: ${SUPPORTED_DEFAULT_APP_PORTS.join(', ')}}` | ||
| ); | ||
| return; | ||
| } | ||
| environment = { | ||
@@ -228,2 +234,9 @@ clientId: answers.clientId, | ||
| if (isUnsupportedDefaultAppPort()) { | ||
| this.info( | ||
| chalk`{red Unsupported port "${port}" for the Official Box CLI app flow. Supported ports: ${SUPPORTED_DEFAULT_APP_PORTS.join(', ')}}` | ||
| ); | ||
| return; | ||
| } | ||
| const environmentName = environment.name; | ||
@@ -242,6 +255,72 @@ const sdkConfig = Object.freeze({ | ||
| const app = express(); | ||
| let callbackHandled = false; | ||
| // Keep run() blocked until callback flow completes. | ||
| // This prevents command exit before the OAuth redirect returns. | ||
| let resolveCallbackFlow; | ||
| const callbackFlowDone = new Promise((resolve) => { | ||
| resolveCallbackFlow = resolve; | ||
| }); | ||
| let callbackTimeout; | ||
| // Timeout and callback may race, so teardown must be idempotent. | ||
| // This guard ensures cleanup and resolve happen only once. | ||
| let callbackFlowResolved = false; | ||
| let server; | ||
| try { | ||
| // Bind only to loopback to avoid exposing callback externally. | ||
| // Browser redirect is local, so external interfaces are unnecessary. | ||
| server = await new Promise((resolve, reject) => { | ||
| const s = app.listen(port, LOOPBACK_HOST); | ||
| s.once('listening', () => resolve(s)); | ||
| s.once('error', reject); | ||
| }); | ||
| } catch (error) { | ||
| if (error.code === 'EADDRINUSE') { | ||
| throw new BoxCLIError( | ||
| `Port ${port} is already in use. Please close the application using this port or use --port to specify a different port.`, | ||
| error | ||
| ); | ||
| } | ||
| throw new BoxCLIError( | ||
| `Failed to start local OAuth server on port ${port}: ${error.message}`, | ||
| error | ||
| ); | ||
| } | ||
| server = app.listen(port); | ||
| const shutdownServer = () => { | ||
| if (!server) { | ||
| return; | ||
| } | ||
| server.close(); | ||
| if (typeof server.closeAllConnections === 'function') { | ||
| server.closeAllConnections(); | ||
| } | ||
| }; | ||
| // Use one finalize path so all exits apply the same cleanup. | ||
| // This keeps timeout and callback completion behavior consistent. | ||
| const finalizeCallbackFlow = () => { | ||
| if (callbackFlowResolved) { | ||
| return; | ||
| } | ||
| callbackFlowResolved = true; | ||
| clearTimeout(callbackTimeout); | ||
| shutdownServer(); | ||
| resolveCallbackFlow(); | ||
| }; | ||
| // Bound callback wait time to avoid hanging sessions forever. | ||
| // If user abandons auth, the command exits predictably. | ||
| callbackTimeout = setTimeout(() => { | ||
| if (callbackHandled) { | ||
| return; | ||
| } | ||
| this.info( | ||
| chalk`{red Login timed out waiting for OAuth callback after ${oauthCallbackTimeoutMs / 1000} seconds.}` | ||
| ); | ||
| finalizeCallbackFlow(); | ||
| }, oauthCallbackTimeoutMs); | ||
| const state = nanoid(32); | ||
@@ -251,2 +330,11 @@ const pkce = useDefaultBoxApp ? generatePKCE() : null; | ||
| app.get('/callback', async (request, res) => { | ||
| // Reject replayed callbacks after a completion was already accepted. | ||
| // This enforces single-use semantics for the local callback endpoint. | ||
| if (callbackHandled) { | ||
| res.status(409).send('OAuth callback already handled.'); | ||
| return; | ||
| } | ||
| callbackHandled = true; | ||
| try { | ||
@@ -276,3 +364,2 @@ if (request.query.state !== state) { | ||
| const client = sdk.getPersistentClient(tokenInfo, tokenCache); | ||
| const user = await client.users.get('me'); | ||
@@ -320,5 +407,7 @@ | ||
| this.info(chalk`{red Login failed: ${errorMessage}}`); | ||
| res.status(500).send( | ||
| 'Login failed. Please check the CLI output for details.' | ||
| ); | ||
| } finally { | ||
| server.close(); | ||
| server.closeAllConnections(); | ||
| finalizeCallbackFlow(); | ||
| } | ||
@@ -367,13 +456,11 @@ }); | ||
| http.get( | ||
| `http://localhost:${port}/callback?state=${authInfo.state}&code=${authInfo.code}` | ||
| `http://${LOOPBACK_HOST}:${port}/callback?state=${authInfo.state}&code=${authInfo.code}` | ||
| ); | ||
| } else { | ||
| if (flags['incognito-browser']) { | ||
| open(authorizeUrl, { | ||
| newInstance: true, | ||
| app: { name: apps.browserPrivate }, | ||
| }); | ||
| } else { | ||
| open(authorizeUrl); | ||
| } | ||
| openAuthorizeInBrowser( | ||
| open, | ||
| apps, | ||
| authorizeUrl, | ||
| flags['incognito-browser'] | ||
| ); | ||
| this.info( | ||
@@ -385,3 +472,3 @@ useDefaultBoxApp | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
| await callbackFlowDone; | ||
| } | ||
@@ -404,3 +491,5 @@ } | ||
| '\n' + | ||
| 'Quickstart: run "box login -d" to sign in immediately. A browser window will open for authorization. Once access is granted, the environment is created and set as default — you can start running commands right away.'; | ||
| 'Quickstart: run "box login -d" to sign in immediately. A browser window will open for authorization. Once access is granted, the environment is created and set as default — you can start running commands right away.\n' + | ||
| '\n' + | ||
| 'Headless environments: use --code (-c) if no browser is available. The CLI will display an authorize URL — visit it in an external browser, authorize and grant access to the app, then provide the state and authorization code back to the CLI when prompted.'; | ||
@@ -411,3 +500,4 @@ OAuthLoginCommand.flags = { | ||
| char: 'c', | ||
| description: 'Manually visit authorize URL and input code', | ||
| description: | ||
| 'Manually provide state and authorization code instead of using a local callback server. Use this in headless environments where no browser is available — the CLI will display an authorize URL to visit externally.', | ||
| default: false, | ||
@@ -459,2 +549,14 @@ }), | ||
| promptForPlatformAppCredentials, | ||
| setOAuthCallbackTimeoutMs(timeoutMs) { | ||
| oauthCallbackTimeoutMs = timeoutMs; | ||
| }, | ||
| resetOAuthCallbackTimeoutMs() { | ||
| oauthCallbackTimeoutMs = OAUTH_CALLBACK_TIMEOUT_MS; | ||
| }, | ||
| setOpenAuthorizeInBrowser(fn) { | ||
| openAuthorizeInBrowser = fn; | ||
| }, | ||
| resetOpenAuthorizeInBrowser() { | ||
| openAuthorizeInBrowser = DEFAULT_OPEN_AUTHORIZE_IN_BROWSER; | ||
| }, | ||
| }; |
@@ -23,2 +23,3 @@ 'use strict'; | ||
| OSSLicensesCommand.noClient = true; | ||
| OSSLicensesCommand.description = | ||
@@ -25,0 +26,0 @@ 'Print a list of open-source licensed packages used in the Box CLI'; |
+41
-0
@@ -33,2 +33,4 @@ 'use strict'; | ||
| }); | ||
| const DEFAULT_SECRET_KEY = 'clientSecret'; | ||
| const DEFAULT_VISIBLE_SECRET_CHARS = 3; | ||
@@ -303,2 +305,39 @@ /** | ||
| function maskSecret(secret, visibleChars = DEFAULT_VISIBLE_SECRET_CHARS) { | ||
| const normalizedVisibleChars = | ||
| _.isInteger(visibleChars) && visibleChars >= 0 | ||
| ? visibleChars | ||
| : DEFAULT_VISIBLE_SECRET_CHARS; | ||
| if (!_.isString(secret) || secret.length <= normalizedVisibleChars) { | ||
| return '*'.repeat(normalizedVisibleChars); | ||
| } | ||
| return `${'*'.repeat(secret.length - normalizedVisibleChars)}${secret.slice(-normalizedVisibleChars)}`; | ||
| } | ||
| function maskObjectValuesByKey( | ||
| value, | ||
| keyToMask = DEFAULT_SECRET_KEY, | ||
| visibleChars = DEFAULT_VISIBLE_SECRET_CHARS | ||
| ) { | ||
| if (_.isArray(value)) { | ||
| return value.map((item) => | ||
| maskObjectValuesByKey(item, keyToMask, visibleChars) | ||
| ); | ||
| } | ||
| if (_.isPlainObject(value)) { | ||
| return _.mapValues(value, (objectValue, key) => { | ||
| if (key === keyToMask && !_.isNil(objectValue)) { | ||
| return maskSecret(objectValue, visibleChars); | ||
| } | ||
| return maskObjectValuesByKey(objectValue, keyToMask, visibleChars); | ||
| }); | ||
| } | ||
| return value; | ||
| } | ||
| module.exports = { | ||
@@ -393,2 +432,4 @@ /** | ||
| }, | ||
| maskSecret, | ||
| maskObjectValuesByKey, | ||
| parseStringToObject, | ||
@@ -395,0 +436,0 @@ checkDir, |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
2670397
19.53%246
0.41%72783
0.19%