|
10 | 10 | except: |
11 | 11 | import pkg_resources |
12 | 12 |
|
| 13 | +# LOC-6719 / INJ-002 (CWE-88): kwargs are forwarded to the BrowserStackLocal |
| 14 | +# binary as flags. Without this allowlist an attacker-influenced kwarg can |
| 15 | +# inject arbitrary flags — notably proxyHost / proxyPort / forceproxy to |
| 16 | +# redirect tunnel traffic through an attacker-controlled proxy. |
| 17 | +# |
| 18 | +# Cross-referenced against the binary's COMMAND_CONFIGURATION table |
| 19 | +# (browserStackTunnel/extensions/node/config/constants.js) and the public docs |
| 20 | +# at https://www.browserstack.com/docs/local-testing/binary-params. Both the |
| 21 | +# camelCase form ('name') and the kebab-case form ('alias') are accepted |
| 22 | +# because user code passes both shapes. Internal-only flags (visible:false: |
| 23 | +# region, bsHost, customRepeater, enterprise, public-interface-services, |
| 24 | +# identifier, trusted-hosts), help/version flags, and undocumented internal |
| 25 | +# flags (-r, -skipCheck, -daemonInstance, -tunnelIdentifier, -uniqueIdentifier) |
| 26 | +# are intentionally excluded. |
| 27 | +# |
| 28 | +# Wrapper-managed keys (key, binarypath, logfile, source) are stripped from |
| 29 | +# self.options in start() before this check runs and are not listed here. |
| 30 | +# 'daemon' / 'logFile' are also omitted because they are emitted unconditionally |
| 31 | +# by _generate_cmd and a user-supplied duplicate would conflict. |
| 32 | +ALLOWED_OPTIONS = frozenset({ |
| 33 | + # Verbose / logging |
| 34 | + 'v', 'vv', 'vvv', 'verbose', |
| 35 | + 'enableLoggingForAPI', 'enable-logging-for-api', |
| 36 | + 'enableUTCLogging', 'enable-utc-logging', |
| 37 | + # Folder testing |
| 38 | + 'f', 'folder', |
| 39 | + # Force / start behaviour |
| 40 | + 'force', 'F', |
| 41 | + 'forcelocal', 'force-local', 'forceLocal', |
| 42 | + 'forceproxy', 'force-proxy', 'forceProxy', |
| 43 | + 'onlyAutomate', 'only-automate', |
| 44 | + # Host targeting / restriction |
| 45 | + 'only', |
| 46 | + 'include-hosts', 'exclude-hosts', |
| 47 | + 'localIdentifier', 'local-identifier', |
| 48 | + 'parallelRuns', 'parallel-runs', |
| 49 | + # Corporate proxy |
| 50 | + 'proxyHost', 'proxy-host', |
| 51 | + 'proxyPort', 'proxy-port', |
| 52 | + 'proxyUser', 'proxy-user', |
| 53 | + 'proxyPass', 'proxy-pass', |
| 54 | + 'disableProxyDiscovery', 'disable-proxy-discovery', |
| 55 | + # Local proxy |
| 56 | + 'localProxyHost', 'local-proxy-host', |
| 57 | + 'localProxyPort', 'local-proxy-port', |
| 58 | + 'localProxyUser', 'local-proxy-user', |
| 59 | + 'localProxyPass', 'local-proxy-pass', |
| 60 | + # PAC / HTTPS / protocol |
| 61 | + 'pacFile', 'pac-file', |
| 62 | + 'https-ports', |
| 63 | + 'client-protocol', |
| 64 | + # CA certificates |
| 65 | + 'useCaCertificate', 'use-ca-certificate', |
| 66 | + 'useSystemInstalledCa', 'use-system-installed-ca', |
| 67 | + # NTLM proxy |
| 68 | + 'ntlm-username', 'ntlm-password', 'ntlm-domain', 'ntlm-workstation', |
| 69 | + # Dashboard / misc |
| 70 | + 'disableDashboard', 'disable-dashboard', |
| 71 | + 'config-file', |
| 72 | + 'no-container', |
| 73 | + 'connect-timeout', |
| 74 | + 'timeout', |
| 75 | + 'debug-utility', 'debug-url', |
| 76 | +}) |
| 77 | + |
13 | 78 | class Local: |
14 | 79 | def __init__(self, key=None, binary_path=None, **kwargs): |
15 | 80 | self.key = os.environ['BROWSERSTACK_ACCESS_KEY'] if 'BROWSERSTACK_ACCESS_KEY' in os.environ else key |
@@ -52,6 +117,8 @@ def get_package_version(self): |
52 | 117 | def _generate_cmd(self): |
53 | 118 | cmd = [self.binary_path, '-d', 'start', '-logFile', self.local_logfile_path, "-k", self.key, '--source', 'python:' + self.get_package_version()] |
54 | 119 | for o in self.options.keys(): |
| 120 | + if o not in ALLOWED_OPTIONS: |
| 121 | + raise BrowserStackLocalError('Unknown option: {}'.format(o)) |
55 | 122 | if self.options.get(o) is not None: |
56 | 123 | cmd = cmd + self.__xstr(o, self.options.get(o)) |
57 | 124 | return cmd |
|
0 commit comments