Repository: appium/appium
Pull request #21233 (2025-04-29 20:27:24)

feat(appium): implement GET /appium/sessions and deprecate GET /sessions
## Proposed changes

This PR adds an implementation for the `GET /appium/sessions` endpoint, according to #20880.
The target branch is intentionally set to `master`, _not_ `appium3` - seeing as `GET /sessions` has already been removed in Appium 3, it is beneficial for Appium 2 users to already have access to its replacement endpoint, for ease of migration.

I confirmed that these changes work using the Inspector (which already has support for this endpoint). Still, it would also be beneficial to add new tests specifically for validating the new `created` field - any suggestions welcome.

## Types of changes

What types of changes does your code introduce to Appium?
_Put an `x` in the boxes that apply_

- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist

_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._

- [x] I have read the [Contributing Guide](https://appium.io/docs/en/latest/contributing/)
- [x] I have signed the CLA
- [x] Lint and unit tests pass locally with my changes
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have added the necessary documentation (if appropriate)
- [ ] Any dependent changes have been merged and published in downstream modules

## Further comments

If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc...



Patch:
diff --git a/packages/appium/docs/en/cli/args.md b/packages/appium/docs/en/cli/args.md
index 8ee5776bc..f41c71c19 100644
--- a/packages/appium/docs/en/cli/args.md
+++ b/packages/appium/docs/en/cli/args.md
@@ -23,14 +23,14 @@ below.
 |--|--|--|--|--|
 |`--address`|IP address to listen on|string|`0.0.0.0`|`-a`|
 |`--allow-cors`|Whether the Appium server should allow web browser connections from any host|boolean|`false`||
-|`--allow-insecure`|Set which insecure features are allowed to run in this server's sessions. Features are defined on a driver level; see documentation for more details. Note that features defined via `--deny-insecure` will be disabled, even if also listed here. If string, a path to a text file containing policy or a comma-delimited list.|array<string>|`[]`||
+|`--allow-insecure`|Set which [insecure features](../guides/security.md) are allowed to run in this server's sessions. Most features are defined on a driver level; see driver documentation for more details. Individual features can be overridden by `--deny-insecure`. Has no effect in combination with `--relaxed-security`.|array<string>|`[]`||
 |`--base-path`|Base path to use as the prefix for all webdriver routes running on the server|string|`""`|`-pa`|
 |`--callback-address`|Callback IP address (default: same as `--address`)|string||`-ca`|
 |`--callback-port`|Callback port (default: same as `--port`) (Value must be between `1` and `65535`)|integer|`4723`|`-cp`|
 |`--config`|Path to an [Appium configuration JSON file](../guides/config.md)|string|||
 |`--debug-log-spacing`|Add exaggerated spacing in logs to help with visual inspection|boolean|`false`||
 |`--default-capabilities`|Set the default desired capabilities, which will be set on each session unless overridden by received capabilities. If a string, a path to a JSON file containing the capabilities, or raw JSON.|object||`-dc`|
-|`--deny-insecure`|Set which insecure features are not allowed to run in this server's sessions. Features are defined on a driver level; see documentation for more details. Features listed here will not be enabled even if also listed in `--allow-insecure`, and even if `--relaxed-security` is enabled. If string, a path to a text file containing policy or a comma-delimited list.|array<string>|`[]`||
+|`--deny-insecure`|Set which [insecure features](../guides/security.md) are not allowed to run in this server's sessions. Most features are defined on a driver level; see driver documentation for more details. Since all insecure features are disabled by default, this argument has no effect without either `--allow-insecure` or `--relaxed-security`, and is applied after both.|array<string>|`[]`||
 |`--driver`|Driver-specific configuration. Keys should correspond to driver package names|object|||
 |`--drivers-import-chunk-size`|The maximum amount of drivers that could be imported in parallel on server startup|number|`3`||
 |`--keep-alive-timeout`|Number of seconds the Appium server should apply as both the keep-alive timeout and the connection timeout for all requests. Setting this to `0` disables the timeout.|integer|`600`|`-ka`|
@@ -48,7 +48,7 @@ below.
 |`--plugin`|Plugin-specific configuration. Keys should correspond to plugin package names|object|||
 |`--plugins-import-chunk-size`|The maximum amount of plugins that could be imported in parallel on server startup|number|`7`||
 |`--port`|Port to listen on (Value must be between `1` and `65535`)|integer|`4723`|`-p`|
-|`--relaxed-security`|Disable additional security checks, so it is possible to use some advanced features, provided by drivers supporting this option. Only enable it if all the clients are in the trusted network and it's not the case if a client could potentially break out of the session sandbox. Specific features can be overridden by using `--deny-insecure`|boolean|`false`||
+|`--relaxed-security`|Allow all [insecure features](../guides/security.md). Only use this if all clients are in a trusted network and could not potentially break out of the session sandbox. Specific features can be overridden by using `--deny-insecure`.|boolean|`false`||
 |`--session-override`|Enables session override (clobbering)|boolean|`false`||
 |`--ssl-cert-path`|Absolute path to the `.cert` file if TLS is used. Must be provided together with `--ssl-key-path`. See the [SSL/TLS/SPDY Support guide](../guides/tls.md) for details|string|||
 |`--ssl-key-path`|Absolute path to the `.key` file if TLS is used. Must be provided together with `--ssl-cert-path`. See the [SSL/TLS/SPDY Support guide](../guides/tls.md) for details|string|||
diff --git a/packages/appium/docs/en/developing/sensitive.md b/packages/appium/docs/en/developing/sensitive.md
new file mode 100644
index 000000000..162ef0b2b
--- /dev/null
+++ b/packages/appium/docs/en/developing/sensitive.md
@@ -0,0 +1,49 @@
+---
+hide:
+  - toc
+
+title: Masking Sensitive Log Data
+---
+
+Since Appium server version 2.18.0 there is a possibility to mask sensitive
+values in logs. The below tutorial explains how to use this feature in third-party
+extensions.
+
+## Why It Might Be Useful
+
+It is the right call to hide sensitive information, like passwords, tokens, etc.
+from server logs, so it does not accidentally leak if these logs end up in wrong hands.
+Appium server already provides a way to manipulate logs records via
+[filtering](../guides/log-filters.md), although it has its own limitations.
+The current approach is more sophisticated though, and requires some fine-tuning
+on the driver/plugin side.
+
+## How To
+
+The assumption is that your extension uses the standard built-in
+[@appium/logger](https://www.npmjs.com/package/@appium/logger).
+In order to get some value in logs replaced by a generic mask it is necessary:
+
+- Change the logging expression to wrap sensitive values and format them, for example:
+
+    ```js
+    this.log.info(`Value: ${value}`);
+    ```
+
+    becomes
+
+    ```js
+    import {logger} from '@appium/support';
+
+    this.log.info('Value: %s', logger.markSensitive(value));
+    ```
+
+    The formatting happens via the standard Node.js's
+    [util.format](https://nodejs.org/api/util.html#utilformatformat-args) API.
+
+- While sending the appropriate server request, where this log line is used and should be masked,
+  add the custom header `X-Appium-Is-Sensitive` with its value set to `1` or `true` (case-insensitive).
+  Without such header the above log value is not going to be masked.
+  This way it is possible to conditionally mask log records depending on which
+  request is being handled by the extension if the log expression is used in the
+  common section.
diff --git a/packages/appium/docs/en/guides/security.md b/packages/appium/docs/en/guides/security.md
index f35098907..ce487f8d0 100644
--- a/packages/appium/docs/en/guides/security.md
+++ b/packages/appium/docs/en/guides/security.md
@@ -27,9 +27,9 @@ Appium when starting it from the command line:
 
 |<div style="width:10em">Parameter</div>|Description|
 |---------------------------------------|-----------|
-|`--relaxed-security`                   |Setting this flag turns on _all_ insecure features (unless blocked by `--deny-insecure`; see below)|
-|`--allow-insecure`                     |Setting this flag to a comma-separated list of feature names or a path to a file containing a feature list (each name on a separate line) will allow _only_ the features listed. For example, `--allow-insecure=adb_shell` will cause _only_ the ADB shell execution feature to be enabled. This is true _unless_ `--relaxed-security` is also used, in which case all features will still be enabled. It makes no sense to combine this flag with `--relaxed-security`.|
-|`--deny-insecure`                      |This flag can likewise be set to a comma-separated list of feature names, or a path to a feature file. Any features listed here will be _disabled_, regardless of whether `--relaxed-security` is set and regardless of whether the names are also listed with `--allow-insecure`.|
+|`--relaxed-security`|Turns on _all_ insecure features (unless blocked by `--deny-insecure`; see below)|
+|`--allow-insecure`|Turns on _only_ specified features. Features can be provided as a comma-separated list of feature names, or in the [Appium Configuration file](./config.md). For example, `--allow-insecure=adb_shell` will cause _only_ the ADB shell execution feature to be enabled. Has no effect when used in combination with `--relaxed-security`.|
+|`--deny-insecure`|Explicitly turns _off_ specified features, overriding `--relaxed-security` and any features specified using `--allow-insecure`. Like `--allow-insecure`, features can be provided as a comma-separated list of feature names, or in the [Appium Configuration file](./config.md).|
 
 ## Insecure Features
 
@@ -44,17 +44,15 @@ might use. Here is an incomplete list of examples from some of Appium's official
 |`record_audio`|Allow recording of host machine audio inputs|XCUITest|
 |`execute_driver_script`| Allows to send a request which has multiple Appium commands.|Execute Driver Plugin|
 
-## Driver-scope security
+Some insecure features operate on the server level, and do not require a driver session:
 
-Since Appium server version 2.12.3 there is a possibility to prefix each feature name with the corresponding automationName where it is going to be applied. For example, if you provide the following features to allow:
-`uiautomator2:adb_shell,xcuitest:get_server_logs,*:record_audio`, then the `adb_shell` would only be allowed
-for the UiAutomator2 driver, `get_server` - for the XCUITest driver and the `record_audio` one - for
-all installed drivers. Feature names provided without explicit automation name prefix are equivalent to
-these having the `*:` prefix. The same feature naming rule applies to the list of denied features.
+|<div style="width:12em">Feature Name</div>|Description|
+|------------|-----------|
+|`session_discovery`|Allows retrieving the list of active server sessions via `GET /appium/sessions`|
 
 ## Examples
 
-To turn on the `get_server_logs` feature for my Appium server, I could start it like this:
+To turn on the `get_server_logs` feature, the Appium server could be started like this:
 
 ```bash
 appium --allow-insecure=get_server_logs
@@ -71,3 +69,29 @@ To allow all features except one:
 ```bash
 appium --relaxed-security --deny-insecure=adb_shell
 ```
+
+## Driver-scope security
+
+Since Appium server version `2.13`, it is possible to apply features on a per-driver basis. This
+can be achieved by prefixing each feature name with the `automationName` of the driver for which
+the feature should be applied. To apply a feature for all drivers, or to apply a server-level
+feature, the wildcard (`*`) prefix should be used.
+
+For example, the server could be started as follows:
+```bash
+appium --allow-insecure=uiautomator2:adb_shell,xcuitest:get_server_logs,*:record_audio
+```
+
+This would result in the following:
+
+* The `adb_shell` feature would be enabled only for the UiAutomator2 driver
+* The `get_server_logs` feature would be enabled only for the XCUITest driver
+* The `record_audio` feature would be enabled for all drivers
+
+Feature provided without an explicit prefix are equivalent to having the wildcard prefix.
+These prefix rules apply to both the `--allow-insecure` and `--deny-insecure` server arguments.
+
+!!! warning
+
+    Starting from Appium 3, the scope prefix is required, and features provided without a scope will
+    raise an error. Note that the behavior of the `--relaxed-security` flag remains unchanged.
diff --git a/packages/appium/docs/mkdocs-en.yml b/packages/appium/docs/mkdocs-en.yml
index 5cc827c58..f2ee0a80d 100644
--- a/packages/appium/docs/mkdocs-en.yml
+++ b/packages/appium/docs/mkdocs-en.yml
@@ -74,6 +74,7 @@ nav:
       - developing/build-plugins.md
       - developing/build-docs.md
       - developing/build-doctor-checks.md
+      - developing/sensitive.md
       - Developer Reference:
           - developing/config-system.md
   - Contributing: contributing.md
diff --git a/packages/appium/lib/appium.js b/packages/appium/lib/appium.js
index 850951b69..7d3d2906b 100644
--- a/packages/appium/lib/appium.js
+++ b/packages/appium/lib/appium.js
@@ -15,16 +15,12 @@ import {
   generateDriverLogPrefix,
 } from '@appium/base-driver';
 import AsyncLock from 'async-lock';
-import {
-  parseCapsForInnerDriver,
-  pullSettings,
-  makeNonW3cCapsError,
-  validateFeatures,
-} from './utils';
+import {parseCapsForInnerDriver, pullSettings, makeNonW3cCapsError} from './utils';
 import {util} from '@appium/support';
 import {getDefaultsForExtension} from './schema';
-import {DRIVER_TYPE, BIDI_BASE_PATH} from './constants';
+import {DRIVER_TYPE, BIDI_BASE_PATH, SESSION_DISCOVERY_FEATURE} from './constants';
 import * as bidiCommands from './bidi-commands';
+import * as insecureFeatures from './insecure-features';
 import * as inspectorCommands from './inspector-commands';
 
 const desiredCapabilityConstraints = /** @type {const} */ ({
@@ -188,6 +184,11 @@ class AppiumDriver extends DriverCore {
     });
   }
 
+  /**
+   * Retrieve information about all active sessions
+   * @returns {Promise<import('@appium/types').MultiSessionData[]>}
+   * @deprecated Use {@linkcode getAppiumSessions} instead
+   */
   async getSessions() {
     return _.toPairs(this.sessions).map(([id, driver]) => ({
       id,
@@ -195,9 +196,18 @@ class AppiumDriver extends DriverCore {
     }));
   }
 
+  /**
+   * Retrieve information about all active sessions.
+   * Results are returned only if the `session_discovery` insecure feature is enabled.
+   * @returns {Promise<import('@appium/types').TimestampedMultiSessionData[]>}
+   */
   async getAppiumSessions () {
-    throw new errors.NotImplementedError('Not implemented yet. ' +
-      'Please check https://github.com/appium/appium/issues/20880 for more details.');
+    this.assertFeatureEnabled(SESSION_DISCOVERY_FEATURE);
+    return _.toPairs(this.sessions).map(([id, driver]) => ({
+      id,
+      created: driver.sessionCreationTimestampMs,
+      capabilities: /** @type {import('@appium/types').DriverCaps<any>} */ (driver.caps),
+    }));
   }
 
   printNewSessionAnnouncement(driverName, driverVersion, driverBaseVersion) {
@@ -327,18 +337,7 @@ class AppiumDriver extends DriverCore {
 
       const driverInstance = /** @type {ExternalDriver} */ (new InnerDriver(this.args, true));
 
-      // We want to assign security values directly on the driver. The driver
-      // should not read security values from `this.opts` because those values
-      // could have been set by a malicious user via capabilities, whereas we
-      // want a guarantee the values were set by the appium server admin
-      if (this.args.relaxedSecurityEnabled) {
-        this.log.info(
-          `Applying relaxed security to '${InnerDriver.name}' as per ` +
-            `server command line argument. All insecure features will be ` +
-            `enabled unless explicitly disabled by --deny-insecure`,
-        );
-        driverInstance.relaxedSecurityEnabled = true;
-      }
+      this.configureDriverFeatures(driverInstance, driverName);
 
       // We also want to assign any new Bidi Commands that the driver has specified, including all
       // the standard bidi commands. But add a method existence guard since some old driver class
@@ -347,18 +346,6 @@ class AppiumDriver extends DriverCore {
         driverInstance.updateBidiCommands(InnerDriver.newBidiCommands ?? {});
       }
 
-      if (!_.isEmpty(this.args.denyInsecure)) {
-        this.log.info('Explicitly preventing use of insecure features:');
-        this.args.denyInsecure.map((a) => this.log.info(`    ${a}`));
-        driverInstance.denyInsecure = validateFeatures(this.args.denyInsecure);
-      }
-
-      if (!_.isEmpty(this.args.allowInsecure)) {
-        this.log.info('Explicitly enabling use of insecure features:');
-        this.args.allowInsecure.map((a) => this.log.info(`    ${a}`));
-        driverInstance.allowInsecure = validateFeatures(this.args.allowInsecure);
-      }
-
       // Likewise, any driver-specific CLI args that were passed in should be assigned directly to
       // the driver so that they cannot be mimicked by a malicious user sending in capabilities
       const cliArgs = this.getCliArgsForDriver(driverName);
@@ -922,6 +909,9 @@ class AppiumDriver extends DriverCore {
   onBidiServerError = bidiCommands.onBidiServerError;
   cleanupBidiSockets = bidiCommands.cleanupBidiSockets;
 
+  configureGlobalFeatures = insecureFeatures.configureGlobalFeatures;
+  configureDriverFeatures = insecureFeatures.configureDriverFeatures;
+
   listCommands = inspectorCommands.listCommands;
   listExtensions = inspectorCommands.listExtensions;
 }
diff --git a/packages/appium/lib/constants.js b/packages/appium/lib/constants.js
index a26b51dbc..bd60a5c6b 100644
--- a/packages/appium/lib/constants.js
+++ b/packages/appium/lib/constants.js
@@ -115,3 +115,8 @@ export const BIDI_BASE_PATH = '/bidi';
  * a bidi socket
  */
 export const BIDI_EVENT_NAME = 'bidiEvent';
+
+/**
+ * The name of the insecure feature that allows retrieving the list of active server sessions
+ */
+export const SESSION_DISCOVERY_FEATURE = 'session_discovery';
diff --git a/packages/appium/lib/insecure-features.ts b/packages/appium/lib/insecure-features.ts
new file mode 100644
index 000000000..5c0750b9a
--- /dev/null
+++ b/packages/appium/lib/insecure-features.ts
@@ -0,0 +1,132 @@
+import _ from 'lodash';
+import logger from './logger';
+
+import type {AppiumDriver} from './appium';
+import type {ExternalDriver} from '@appium/types';
+
+const ALL_DRIVERS_MATCH = '*';
+const FEATURE_NAME_SEPARATOR = ':';
+
+/**
+ * Configures insecure features according to the values in `args.relaxedSecurityEnabled`,
+ * `args.allowInsecure`, and `args.denyInsecure`, and informs the user about any
+ * globally-applied features.
+ * Uses `logger` instead of `this.log` to reduce user confusion.
+ */
+export function configureGlobalFeatures(this: AppiumDriver) {
+  if (this.args.relaxedSecurityEnabled) {
+    logger.info(
+      `Enabling relaxed security. All insecure features will be ` +
+        `enabled unless explicitly disabled by --deny-insecure`,
+    );
+    this.relaxedSecurityEnabled = true;
+  } else if (!_.isEmpty(this.args.allowInsecure)) {
+    this.allowInsecure = validateFeatures(this.args.allowInsecure);
+    const globalAllowedFeatures = filterInsecureFeatures(this.allowInsecure);
+    if (!_.isEmpty(globalAllowedFeatures)) {
+      logger.info('Explicitly enabling insecure features:');
+      globalAllowedFeatures.forEach((a) => logger.info(`    ${a}`));
+    }
+  }
+  if (_.isEmpty(this.args.denyInsecure)) {
+    return;
+  }
+  this.denyInsecure = validateFeatures(this.args.denyInsecure);
+  const globalDeniedFeatures = filterInsecureFeatures(this.denyInsecure);
+  if (_.isEmpty(globalDeniedFeatures)) {
+    return;
+  }
+  logger.info('Explicitly disabling insecure features:');
+  globalDeniedFeatures.forEach((a) => logger.info(`    ${a}`));
+}
+
+/**
+ * If anything in the umbrella driver's insecure feature configuration applies to this driver,
+ * assign it to the driver instance
+ *
+ * @param driver
+ * @param driverName
+ */
+export function configureDriverFeatures(
+  this: AppiumDriver,
+  driver: ExternalDriver,
+  driverName: string,
+) {
+  if (this.relaxedSecurityEnabled) {
+    this.log.info(
+      `Enabling relaxed security for this session as per the server configuration. ` +
+        `All insecure features will be enabled unless explicitly disabled by --deny-insecure`,
+    );
+    driver.relaxedSecurityEnabled = true;
+  }
+  const allowedDriverFeatures = filterInsecureFeatures(this.allowInsecure, driverName);
+  if (!_.isEmpty(allowedDriverFeatures)) {
+    this.log.info('Explicitly enabling insecure features for this session ' +
+      'as per the server configuration:',
+    );
+    allowedDriverFeatures.forEach((a) => this.log.info(`    ${a}`));
+    driver.allowInsecure = allowedDriverFeatures;
+  }
+  const deniedDriverFeatures = filterInsecureFeatures(this.denyInsecure, driverName);
+  if (_.isEmpty(deniedDriverFeatures)) {
+    return;
+  }
+  this.log.info('Explicitly disabling insecure features for this session ' +
+    'as per the server configuration:',
+  );
+  deniedDriverFeatures.forEach((a) => this.log.info(`    ${a}`));
+  driver.denyInsecure = deniedDriverFeatures;
+}
+
+/**
+ * Validates the list of allowed/denied server features
+ *
+ * @param features
+ */
+function validateFeatures(features: string[]): string[] {
+  const validator = (fullName: string) => {
+    const separatorPos = fullName.indexOf(FEATURE_NAME_SEPARATOR);
+    // TODO: This is for the backward compatibility with Appium2
+    // TODO: In Appium3 the separator will be mandatory
+    if (separatorPos < 0) {
+      return `${ALL_DRIVERS_MATCH}${FEATURE_NAME_SEPARATOR}${fullName}`;
+    }
+
+    const [automationName, featureName] = [
+      fullName.substring(0, separatorPos),
+      fullName.substring(separatorPos + 1)
+    ];
+    if (!automationName || !featureName) {
+      throw new Error(
+        `The full feature name must include both the destination automation name or the ` +
+        `'${ALL_DRIVERS_MATCH}' wildcard to apply the feature to all installed drivers, and ` +
+        `the feature name split by a colon, got '${fullName}' instead`
+      );
+    }
+    return fullName;
+  };
+  return features.map(validator);
+}
+
+/**
+ * Filters the list of insecure features to only those that are
+ * applicable to the given driver name.
+ * Assumes that all feature names have already been validated
+ *
+ * @param features
+ * @param driverName
+ */
+function filterInsecureFeatures(
+  features: string[],
+  driverName: string = ALL_DRIVERS_MATCH
+): string[] {
+  const filterFn = (fullName: string) => {
+    const separatorPos = fullName.indexOf(FEATURE_NAME_SEPARATOR);
+    if (separatorPos <= 0) {
+      return false;
+    }
+    const automationName = fullName.substring(0, separatorPos);
+    return [driverName, ALL_DRIVERS_MATCH].includes(automationName);
+  };
+  return features.filter(filterFn);
+}
diff --git a/packages/appium/lib/main.js b/packages/appium/lib/main.js
index 2726690f9..b646d32d7 100755
--- a/packages/appium/lib/main.js
+++ b/packages/appium/lib/main.js
@@ -375,6 +375,9 @@ async function main(args) {
 
   await logStartupInfo(parsedArgs);
 
+  // handle the insecure feature configuration since some features may apply globally
+  appiumDriver.configureGlobalFeatures();
+
   const appiumHomeSourceName = determineAppiumHomeSource(args?.appiumHome);
   logger.debug(`The ${appiumHomeSourceName}: ${appiumHome}`);
 
diff --git a/packages/appium/lib/utils.js b/packages/appium/lib/utils.js
index 914a9e1c7..910fb8cf4 100644
--- a/packages/appium/lib/utils.js
+++ b/packages/appium/lib/utils.js
@@ -17,8 +17,6 @@ const STANDARD_CAPS_LOWERCASE = new Set([...STANDARD_CAPS].map((cap) => cap.toLo
 export const V4_BROADCAST_IP = '0.0.0.0';
 export const V6_BROADCAST_IP = '::';
 export const npmPackage = fs.readPackageJsonFrom(__dirname);
-const ALL_DRIVERS_MATCH = '*';
-const FEATURE_NAME_SEPARATOR = ':';
 
 /**
  *
@@ -463,37 +461,6 @@ export function isBroadcastIp(address) {
   return [V4_BROADCAST_IP, V6_BROADCAST_IP, `[${V6_BROADCAST_IP}]`].includes(address);
 }
 
-/**
- * Validates the list of allowed/denied server features
- *
- * @param {string[]} features
- * @returns {string[]}
- */
-export function validateFeatures(features) {
-  const validator = (/** @type {string} */ fullName) => {
-    const separatorPos = fullName.indexOf(FEATURE_NAME_SEPARATOR);
-    // TODO: This is for the backward compatibility with Appium2
-    // TODO: In Appium3 the separator will be mandatory
-    if (separatorPos < 0) {
-      return `${ALL_DRIVERS_MATCH}${FEATURE_NAME_SEPARATOR}${fullName}`;
-    }
-
-    const [automationName, featureName] = [
-      fullName.substring(0, separatorPos),
-      fullName.substring(separatorPos + 1)
-    ];
-    if (!automationName || !featureName) {
-      throw new Error(
-        `The full feature name must include both the destination automation name or the ` +
-        `'${ALL_DRIVERS_MATCH}' wildcard to apply the feature to all installed drivers, and ` +
-        `the feature name split by a colon, got '${fullName}' instead`
-      );
-    }
-    return fullName;
-  };
-  return features.map(validator);
-}
-
 /**
  * @typedef {import('@appium/types').StringRecord} StringRecord
  * @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints
diff --git a/packages/base-driver/lib/basedriver/core.ts b/packages/base-driver/lib/basedriver/core.ts
index 3f088c49f..79b592f24 100644
--- a/packages/base-driver/lib/basedriver/core.ts
+++ b/packages/base-driver/lib/basedriver/core.ts
@@ -41,6 +41,8 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
 
   sessionId: string | null;
 
+  sessionCreationTimestampMs: number;
+
   opts: DriverOpts<C>;
 
   initialOpts: InitialOpts;
@@ -314,7 +316,7 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
         `Potentially insecure feature '${name}' has not been ` +
           `enabled. If you want to enable this feature and accept ` +
           `the security ramifications, please do so by following ` +
-          `the documented instructions at http://appium.io/docs/en/2.0/guides/security/`,
+          `the documented instructions at http://appium.io/docs/en/latest/guides/security/`,
       );
     }
   }
diff --git a/packages/base-driver/lib/basedriver/driver.ts b/packages/base-driver/lib/basedriver/driver.ts
index 277138bb8..d64d68425 100644
--- a/packages/base-driver/lib/basedriver/driver.ts
+++ b/packages/base-driver/lib/basedriver/driver.ts
@@ -302,6 +302,7 @@ export class BaseDriver<
     this.validateDesiredCaps(caps);
 
     this.sessionId = util.uuidV4();
+    this.sessionCreationTimestampMs = Date.now();
     this.caps = caps;
     // merge caps onto opts so we don't need to worry about what's where
     this.opts = {..._.cloneDeep(this.initialOpts), ...this.caps};
@@ -345,6 +346,11 @@ export class BaseDriver<
 
     return [this.sessionId, caps] as CreateResult;
   }
+
+  /**
+   * Returns the session id and capabilities for the session
+   * @deprecated Use AppiumDriver.getAppiumSessions instead for getting the session data.
+   */
   async getSessions() {
     const ret: MultiSessionData<C>[] = [];
 
@@ -360,7 +366,7 @@ export class BaseDriver<
 
   /**
    * Returns capabilities for the session and event history (if applicable)
-   * @deprecated Use {@linkcode ISessionCommands.getAppiumSessionCapabilities} instead for getting the capabilities.
+   * @deprecated Use {@linkcode getAppiumSessionCapabilities} instead for getting the capabilities.
    * Use {@linkcode EventCommands.getLogEvents} instead to get the event history.
    */
   async getSession() {
diff --git a/packages/types/lib/driver.ts b/packages/types/lib/driver.ts
index 865e0398e..f67f0a983 100644
--- a/packages/types/lib/driver.ts
+++ b/packages/types/lib/driver.ts
@@ -183,13 +183,29 @@ export interface IExecuteCommands {
   ): Promise<TReturn>;
 }
 
+/**
+ * Data returned by {@linkcode ISessionHandler.getSessions}.
+ *
+ * @typeParam C - The driver's constraints
+ */
 export interface MultiSessionData<C extends Constraints = Constraints> {
   id: string;
   capabilities: DriverCaps<C>;
 }
 
 /**
- * Data returned by {@linkcode ISessionCommands.getSession}.
+ * Data returned by `AppiumDriver.getAppiumSessions`
+ *
+ * @typeParam C - The driver's constraints
+ */
+export interface TimestampedMultiSessionData<C extends Constraints = Constraints> {
+  id: string;
+  created: number; // Unix timestamp in milliseconds
+  capabilities: DriverCaps<C>;
+}
+
+/**
+ * Data returned by {@linkcode ISessionHandler.getSession}.
  *
  * @typeParam C - The driver's constraints
  * @typeParam T - Any extra data the driver stuffs in here
@@ -610,6 +626,7 @@ export interface DriverStatus {
 export interface Core<C extends Constraints, Settings extends StringRecord = StringRecord> {
   shouldValidateCaps: boolean;
   sessionId: string | null;
+  sessionCreationTimestampMs: number;
   opts: DriverOpts<C>;
   initialOpts: InitialOpts;
   protocol?: Protocol;


Test patch:
diff --git a/packages/appium/test/unit/appiumdriver.spec.js b/packages/appium/test/unit/appiumdriver.spec.js
index 4d091c366..915455d6f 100644
--- a/packages/appium/test/unit/appiumdriver.spec.js
+++ b/packages/appium/test/unit/appiumdriver.spec.js
@@ -1,6 +1,6 @@
 // @ts-check
 
-import {PLUGIN_TYPE} from '../../lib/constants';
+import {PLUGIN_TYPE, SESSION_DISCOVERY_FEATURE} from '../../lib/constants';
 import B from 'bluebird';
 import {BaseDriver} from '@appium/base-driver';
 import {FakeDriver} from '@appium/fake-driver';
@@ -14,6 +14,7 @@ import {rewiremock, BASE_CAPS, W3C_CAPS, W3C_PREFIXED_CAPS} from '../helpers';
 import {BasePlugin} from '@appium/base-plugin';
 
 const SESSION_ID = '1';
+const SESSION_DISCOVERY_ENABLED = {allowInsecure: [`*:${SESSION_DISCOVERY_FEATURE}`]};
 
 describe('AppiumDriver', function () {
   /** @type {import('sinon').SinonSandbox} */
@@ -106,13 +107,51 @@ describe('AppiumDriver', function () {
 
       return [appium, mockFakeDriver];
     }
+    describe('configureGlobalFeatures', function () {
+      /** @type {AppiumDriver} */
+      let appium;
+
+      /**
+       * @param {import('@appium/types').DriverOpts<import('../../lib/appium').AppiumDriverConstraints>} cliArgs
+       */
+      function createDriver(cliArgs) {
+        appium = new AppiumDriver(cliArgs);
+        appium.configureGlobalFeatures();
+      };
+      it('should not allow insecure features by default', function () {
+        createDriver({});
+        appium.allowInsecure.should.be.empty;
+        appium.denyInsecure.should.be.empty;
+        appium.relaxedSecurityEnabled.should.be.false;
+      });
+      it('should allow insecure features', function () {
+        createDriver({allowInsecure: ['foo:bar']});
+        appium.allowInsecure.should.eql(['foo:bar']);
+      });
+      it('should deny insecure features', function () {
+        createDriver({denyInsecure: ['foo:baz']});
+        appium.denyInsecure.should.eql(['foo:baz']);
+      });
+      it('should allow relaxed security', function () {
+        createDriver({relaxedSecurityEnabled: true});
+        appium.relaxedSecurityEnabled.should.be.true;
+      });
+      it('should ignore allowed features in combination with relaxed security', function () {
+        createDriver({
+          allowInsecure: ['foo:bar'],
+          relaxedSecurityEnabled: true,
+        });
+        appium.allowInsecure.should.be.empty;
+        appium.relaxedSecurityEnabled.should.be.true;
+      });
+    });
     describe('createSession', function () {
       /** @type {AppiumDriver} */
       let appium;
       /** @type {sinon.SinonMock} */
       let mockFakeDriver;
       beforeEach(function () {
-        [appium, mockFakeDriver] = getDriverAndFakeDriver();
+        [appium, mockFakeDriver] = getDriverAndFakeDriver(SESSION_DISCOVERY_ENABLED);
       });
       afterEach(async function () {
         mockFakeDriver.restore();
@@ -157,6 +196,7 @@ describe('AppiumDriver', function () {
         mockFakeDriver.verify();
       });
       it('should kill all other sessions if sessionOverride is on', async function () {
+        appium.configureGlobalFeatures();
         appium.args.sessionOverride = true;
 
         // mock three sessions that should be removed when the new one is created
@@ -172,7 +212,7 @@ describe('AppiumDriver', function () {
         appium.sessions['xyz-321-abc'] = fakeDrivers[1];
         appium.sessions['123-abc-xyz'] = fakeDrivers[2];
 
-        let sessions = await appium.getSessions();
+        let sessions = await appium.getAppiumSessions();
         sessions.should.have.length(3);
 
         mockFakeDriver
@@ -182,7 +222,7 @@ describe('AppiumDriver', function () {
           .returns([SESSION_ID, removeAppiumPrefixes(W3C_PREFIXED_CAPS)]);
         await appium.createSession(undefined, null, W3C_CAPS);
 
-        sessions = await appium.getSessions();
+        sessions = await appium.getAppiumSessions();
         sessions.should.have.length(1);
 
         for (let mfd of mockFakeDrivers) {
@@ -258,17 +298,18 @@ describe('AppiumDriver', function () {
       let appium;
       let mockFakeDriver;
       beforeEach(function () {
-        [appium, mockFakeDriver] = getDriverAndFakeDriver();
+        [appium, mockFakeDriver] = getDriverAndFakeDriver(SESSION_DISCOVERY_ENABLED);
       });
       afterEach(function () {
         mockFakeDriver.restore();
       });
       it('should remove the session if it is found', async function () {
+        appium.configureGlobalFeatures();
         let [sessionId] = (await appium.createSession(null, null, W3C_CAPS)).value;
-        let sessions = await appium.getSessions();
+        let sessions = await appium.getAppiumSessions();
         sessions.should.have.length(1);
         await appium.deleteSession(sessionId);
-        sessions = await appium.getSessions();
+        sessions = await appium.getAppiumSessions();
         sessions.should.have.length(0);
       });
       it("should call inner driver's deleteSession method", async function () {
@@ -281,11 +322,73 @@ describe('AppiumDriver', function () {
         await mockFakeDriver.object.deleteSession();
       });
     });
-    describe('getSessions', function () {
+    describe('configureDriverFeatures', function () {
+      /** @type {AppiumDriver} */
+      let appium;
+
+      /**
+       *
+       * @param {import('@appium/types').DriverOpts<import('../../lib/appium').AppiumDriverConstraints>} appiumArgs
+       * @returns {Promise<FakeDriver>}
+       */
+      async function getDriverInstance(appiumArgs) {
+        appium = new AppiumDriver(appiumArgs);
+        appium.configureGlobalFeatures();
+        const fakeDriver = new FakeDriver();
+        const mockFakeDriver = sandbox.mock(fakeDriver);
+        const mockedDriverReturnerClass = function Driver() {
+          return fakeDriver;
+        };
+
+        appium.driverConfig = {
+          findMatchingDriver: sandbox.stub().returns({
+            driver: mockedDriverReturnerClass,
+            version: '1.2.3',
+            driverName: 'fake',
+          }),
+        };
+
+        mockFakeDriver
+          .expects('createSession')
+          .once()
+          .withExactArgs(undefined, null, W3C_CAPS, [])
+          .returns([SESSION_ID, removeAppiumPrefixes(W3C_PREFIXED_CAPS)]);
+        await appium.createSession(undefined, null, W3C_CAPS);
+
+        return fakeDriver;
+      }
+      afterEach(async function () {
+        await appium.deleteSession(SESSION_ID);
+      });
+      it(`should not apply any insecure features by default`, async function () {
+        fakeDriver = await getDriverInstance({});
+        fakeDriver.allowInsecure.should.be.empty;
+        fakeDriver.denyInsecure.should.be.empty;
+        fakeDriver.relaxedSecurityEnabled.should.be.false;
+      });
+      it(`should apply relaxed security`, async function () {
+        fakeDriver = await getDriverInstance({relaxedSecurityEnabled: true});;
+        fakeDriver.relaxedSecurityEnabled.should.be.true;
+      });
+      it(`should apply global-scope insecure features`, async function () {
+        fakeDriver = await getDriverInstance({
+          allowInsecure: ['*:foo'],
+          denyInsecure: ['*:bar'],
+        });
+        fakeDriver.allowInsecure.should.eql(['*:foo']);
+        fakeDriver.denyInsecure.should.eql(['*:bar']);
+      });
+      it(`should apply driver-scope insecure features only if the driver name matches`, async function () {
+        fakeDriver = await getDriverInstance({allowInsecure: ['fake:foo', 'real:bar']});
+        fakeDriver.allowInsecure.should.eql(['fake:foo']);
+      });
+    });
+    describe('getAppiumSessions', function () {
       let appium, mockFakeDriver;
       let sessions;
       before(function () {
-        [appium, mockFakeDriver] = getDriverAndFakeDriver();
+        [appium, mockFakeDriver] = getDriverAndFakeDriver(SESSION_DISCOVERY_ENABLED);
+        appium.configureGlobalFeatures();
       });
       afterEach(async function () {
         for (let session of sessions) {
@@ -294,7 +397,7 @@ describe('AppiumDriver', function () {
         mockFakeDriver.restore();
       });
       it('should return an empty array of sessions', async function () {
-        sessions = await appium.getSessions();
+        sessions = await appium.getAppiumSessions();
         sessions.should.be.an('array');
         sessions.should.be.empty;
       });
@@ -316,12 +419,14 @@ describe('AppiumDriver', function () {
           .returns(['fake-session-id-2', removeAppiumPrefixes(caps2.alwaysMatch)]);
         let [session2Id, session2Caps] = (await appium.createSession(null, null, caps2)).value;
 
-        sessions = await appium.getSessions();
+        sessions = await appium.getAppiumSessions();
         sessions.should.be.an('array');
         sessions.should.have.length(2);
         sessions[0].id.should.equal(session1Id);
+        sessions[0].should.have.property('created');
         removeAppiumPrefixes(caps1.alwaysMatch).should.eql(session1Caps);
         sessions[1].id.should.equal(session2Id);
+        sessions[1].should.have.property('created');
         removeAppiumPrefixes(caps2.alwaysMatch).should.eql(session2Caps);
       });
     });
diff --git a/packages/base-driver/test/unit/basedriver/capability.spec.js b/packages/base-driver/test/unit/basedriver/capability.spec.js
index 70715ec29..834f5c116 100644
--- a/packages/base-driver/test/unit/basedriver/capability.spec.js
+++ b/packages/base-driver/test/unit/basedriver/capability.spec.js
@@ -158,8 +158,9 @@ describe('Desired Capabilities', function () {
       });
       logWarnSpy.called.should.be.true;
 
-      let sessions = await d.getSessions();
-      sessions[0].capabilities.noReset.should.eql(false);
+      let sessionCaps = await d.getAppiumSessionCapabilities();
+      // @ts-expect-error the 'noReset' property should exist
+      sessionCaps.capabilities.noReset.should.eql(false);
     });
 
     it('should allow a string "true"', async function () {
@@ -173,8 +174,9 @@ describe('Desired Capabilities', function () {
       });
       logWarnSpy.called.should.be.true;
 
-      let sessions = await d.getSessions();
-      sessions[0].capabilities.noReset.should.eql(true);
+      let sessionCaps = await d.getAppiumSessionCapabilities();
+      // @ts-expect-error the 'noReset' property should exist
+      sessionCaps.capabilities.noReset.should.eql(true);
     });
 
     it('should allow a string "true" in string capabilities', async function () {
@@ -187,8 +189,9 @@ describe('Desired Capabilities', function () {
       });
       logWarnSpy.called.should.be.false;
 
-      let sessions = await d.getSessions();
-      sessions[0].capabilities.language.should.eql('true');
+      let sessionCaps = await d.getAppiumSessionCapabilities();
+      // @ts-expect-error the 'language' property should exist
+      sessionCaps.capabilities.language.should.eql('true');
     });
   });
 
@@ -204,8 +207,9 @@ describe('Desired Capabilities', function () {
       });
       logWarnSpy.called.should.be.true;
 
-      let sessions = await d.getSessions();
-      sessions[0].capabilities.newCommandTimeout.should.eql(1);
+      let sessionCaps = await d.getAppiumSessionCapabilities();
+      // @ts-expect-error the 'newCommandTimeout' property should exist
+      sessionCaps.capabilities.newCommandTimeout.should.eql(1);
     });
 
     it('should allow a string "1.1"', async function () {
@@ -219,8 +223,9 @@ describe('Desired Capabilities', function () {
       });
       logWarnSpy.called.should.be.true;
 
-      let sessions = await d.getSessions();
-      sessions[0].capabilities.newCommandTimeout.should.eql(1.1);
+      let sessionCaps = await d.getAppiumSessionCapabilities();
+      // @ts-expect-error the 'newCommandTimeout' property should exist
+      sessionCaps.capabilities.newCommandTimeout.should.eql(1.1);
     });
 
     it('should allow a string "1" in string capabilities', async function () {
@@ -233,8 +238,9 @@ describe('Desired Capabilities', function () {
       });
       logWarnSpy.called.should.be.false;
 
-      let sessions = await d.getSessions();
-      sessions[0].capabilities.language.should.eql('1');
+      let sessionCaps = await d.getAppiumSessionCapabilities();
+      // @ts-expect-error the 'language' property should exist
+      sessionCaps.capabilities.language.should.eql('1');
     });
   });
 
diff --git a/packages/driver-test-support/lib/unit-suite.js b/packages/driver-test-support/lib/unit-suite.js
index f0e1f0643..3542566ac 100644
--- a/packages/driver-test-support/lib/unit-suite.js
+++ b/packages/driver-test-support/lib/unit-suite.js
@@ -93,26 +93,6 @@ export function driverUnitTestSuite(
       caps.should.equal(await d.getSession());
     });
 
-    it('should return sessions if no session exists', async function () {
-      let sessions = await d.getSessions();
-      sessions.length.should.equal(0);
-    });
-
-    it('should return sessions', async function () {
-      const caps = _.cloneDeep(w3cCaps);
-      await d.createSession(caps);
-      let sessions = await d.getSessions();
-
-      sessions.length.should.equal(1);
-      sessions[0].should.include({
-        id: d.sessionId,
-      });
-      sessions[0].capabilities.should.include({
-        deviceName: 'Commodore 64',
-        platformName: 'Fake',
-      });
-    });
-
     it('should fulfill an unexpected driver quit promise', async function () {
       // make a command that will wait a bit so we can crash while it's running
       sandbox.stub(d, 'getStatus').callsFake(async () => {