Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/configuring-the-deploy-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,35 @@ Boolean. When enabled, exports actual secret values (e.g. connection `client_sec

> **Warning:** Enabling this option will write real credentials to exported files. Use with caution in shared or version-controlled environments.

### `AUTH0_ALLOW_EXTERNAL_CODE_PATHS`

Boolean. When enabled, allows code files referenced in resource configurations (such as action code, hook scripts, rule scripts, and database custom scripts) to be loaded from paths outside the config root directory. Default: `false`.

**Background:** The Deploy CLI enforces that all file path references in resource configurations must resolve within the config root directory as a security measure to prevent unintended file access. When a path resolves outside the config root, a deprecation warning is emitted and the file is still loaded. **In the next major release this will become a hard error.** If you see this warning, move the referenced file inside your config directory before upgrading.

**Monorepo use case:** In some project structures — particularly monorepos — action code or scripts are intentionally stored in a shared directory that sits above the config root. For example:

```
my-monorepo/
├── shared/
│ └── action-code.js # shared code, lives outside the config root
└── auth0-config/ # AUTH0_INPUT_FILE points here
└── actions/
└── my-action.json # references "../../shared/action-code.js"
```

In this case the path check would emit a deprecation warning (and will block deployment in the next major release). Setting `AUTH0_ALLOW_EXTERNAL_CODE_PATHS=true` explicitly opts out of the enforcement, suppressing the warning and allowing these cross-directory references to load successfully.

> **Security notice:** This flag bypasses a security boundary introduced to prevent path traversal. Only enable it if your project structure genuinely requires loading files from outside the config directory, and ensure you trust all config files being processed. **Do not enable this flag in environments where config files are sourced from untrusted or unreviewed input.**

#### Example

```json
{
"AUTH0_ALLOW_EXTERNAL_CODE_PATHS": true
}
```

### `EXCLUDED_PROPS`

Provides ability to exclude any unwanted properties from management.
Expand Down
35 changes: 20 additions & 15 deletions src/context/directory/handlers/actionModules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';
import fs from 'fs-extra';
import { constants } from '../../../tools';
import { constants, loadFileAndReplaceKeywords } from '../../../tools';

import { getFiles, existsMustBeDir, loadJSON, sanitize, dumpJSON } from '../../../utils';
import log from '../../../logger';
Expand All @@ -24,22 +24,27 @@ function parse(context: DirectoryContext): ParsedActionModules {
disableKeywordReplacement: context.disableKeywordReplacement,
}),
};
const moduleFolder = path.join(constants.ACTION_MODULES_DIRECTORY, `${module.name}`);

if (module.code) {
// The `module.code` can be a file path. It needs to be loaded.
// It can be a relative path, so we need to handle both cases.
const unixPath = module.code.replace(/[\\/]+/g, '/').replace(/^([a-zA-Z]+:|\.\/)/, '');
if (fs.existsSync(unixPath)) {
log.warn(
`Support for absolute paths and paths outside the config root will be deprecated in a future version to improve the security of the tool. ` +
`Please update your configuration to use paths relative to the config directory. ` +
`Current absolute path used: ["${module.code}"]`
);
module.code = context.loadFile(unixPath, moduleFolder);
} else {
module.code = context.loadFile(path.join(context.filePath, module.code), moduleFolder);
const normalizedCode = module.code.replace(/\\/g, '/');
const configRoot = path.resolve(context.filePath);
const resolvedPath = path.resolve(context.filePath, normalizedCode);
if (!resolvedPath.startsWith(configRoot + path.sep)) {
if (context.config.AUTH0_ALLOW_EXTERNAL_CODE_PATHS) {
log.debug(
`Loading file outside config directory (AUTH0_ALLOW_EXTERNAL_CODE_PATHS enabled): "${module.code}"`
);
} else {
log.warn(
`Path "${module.code}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
`This will be blocked as an error in the next major release. ` +
`Move the file inside your config directory or set AUTH0_ALLOW_EXTERNAL_CODE_PATHS=true to allow it.`
);
}
}
module.code = loadFileAndReplaceKeywords(resolvedPath, {
mappings: context.mappings,
disableKeywordReplacement: context.disableKeywordReplacement,
});
}

return module;
Expand Down
36 changes: 20 additions & 16 deletions src/context/directory/handlers/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable consistent-return */
import path from 'path';
import fs from 'fs-extra';
import { constants } from '../../../tools';
import { constants, loadFileAndReplaceKeywords } from '../../../tools';

import { getFiles, existsMustBeDir, loadJSON, sanitize, dumpJSON } from '../../../utils';
import log from '../../../logger';
Expand All @@ -25,23 +25,27 @@ function parse(context: DirectoryContext): ParsedActions {
disableKeywordReplacement: context.disableKeywordReplacement,
}),
};
const actionFolder = path.join(constants.ACTIONS_DIRECTORY, `${action.name}`);

if (action.code) {
// Convert `action.code` path to Unix-style path by replacing backslashes and multiple slashes with a single forward slash, and remove leading drive letters or './'.
const unixPath = action.code.replace(/[\\/]+/g, '/').replace(/^([a-zA-Z]+:|\.\/)/, '');
if (fs.existsSync(unixPath)) {
// If the Unix-style path exists, load the file from that path
log.warn(
`Support for absolute paths and paths outside the config root will be deprecated in a future version to improve the security of the tool. ` +
`Please update your configuration to use paths relative to the config directory. ` +
`Current absolute path used: ["${action.code}"]`
);
action.code = context.loadFile(unixPath, actionFolder);
} else {
// Otherwise, load the file from the context's file path
action.code = context.loadFile(path.join(context.filePath, action.code), actionFolder);
const normalizedCode = action.code.replace(/\\/g, '/');
const configRoot = path.resolve(context.filePath);
const resolvedPath = path.resolve(context.filePath, normalizedCode);
if (!resolvedPath.startsWith(configRoot + path.sep)) {
if (context.config.AUTH0_ALLOW_EXTERNAL_CODE_PATHS) {
log.debug(
`Loading file outside config directory (AUTH0_ALLOW_EXTERNAL_CODE_PATHS enabled): "${action.code}"`
);
} else {
log.warn(
`Path "${action.code}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
`This will be blocked as an error in the next major release. ` +
`Move the file inside your config directory or set AUTH0_ALLOW_EXTERNAL_CODE_PATHS=true to allow it.`
);
}
}
action.code = loadFileAndReplaceKeywords(resolvedPath, {
mappings: context.mappings,
disableKeywordReplacement: context.disableKeywordReplacement,
});
}

return action;
Expand Down
34 changes: 23 additions & 11 deletions src/context/directory/handlers/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ type DatabaseMetadata = {
function getDatabase(
folder: string,
configRoot: string,
mappingOpts: { mappings: KeywordMappings; disableKeywordReplacement: boolean }
mappingOpts: { mappings: KeywordMappings; disableKeywordReplacement: boolean },
allowExternalPaths?: boolean
): {} {
const metaFile = path.join(folder, 'database.json');

Expand Down Expand Up @@ -70,13 +71,19 @@ function getDatabase(
log.warn('Skipping invalid database configuration: ' + name);
} else {
const resolvedBase = path.resolve(configRoot);
const toLoad = path.resolve(folder, script);
const toLoad = path.resolve(folder, script.replace(/\\/g, '/'));
if (!toLoad.startsWith(resolvedBase + path.sep)) {
log.warn(
`Support for absolute paths and paths outside the config root will be deprecated in a future version to improve the security of the tool. ` +
`Please update your configuration to use paths relative to the config directory. ` +
`Current absolute path used: ["${script}"]`
);
if (allowExternalPaths) {
log.debug(
`Loading file outside config directory (AUTH0_ALLOW_EXTERNAL_CODE_PATHS enabled): "${script}"`
);
} else {
log.warn(
`Path "${script}" resolves to "${toLoad}" which is outside the config directory "${resolvedBase}". ` +
`This will be blocked as an error in the next major release. ` +
`Move the file inside your config directory or set AUTH0_ALLOW_EXTERNAL_CODE_PATHS=true to allow it.`
);
}
}
database.options.customScripts[name] = loadFileAndReplaceKeywords(toLoad, mappingOpts);
}
Expand All @@ -97,10 +104,15 @@ function parse(context: DirectoryContext): ParsedDatabases {

const databases = folders
.map((f) =>
getDatabase(f, context.filePath, {
mappings: context.mappings,
disableKeywordReplacement: context.disableKeywordReplacement,
})
getDatabase(
f,
context.filePath,
{
mappings: context.mappings,
disableKeywordReplacement: context.disableKeywordReplacement,
},
context.config.AUTH0_ALLOW_EXTERNAL_CODE_PATHS
)
)
.filter((p) => Object.keys(p).length > 1);

Expand Down
27 changes: 25 additions & 2 deletions src/context/directory/handlers/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';
import fs from 'fs-extra';
import { constants } from '../../../tools';
import { constants, loadFileAndReplaceKeywords } from '../../../tools';

import { getFiles, existsMustBeDir, dumpJSON, loadJSON, sanitize } from '../../../utils';
import log from '../../../logger';
Expand All @@ -24,7 +24,30 @@ function parse(context: DirectoryContext): ParsedHooks {
}),
};
if (hook.script) {
hook.script = context.loadFile(hook.script, constants.HOOKS_DIRECTORY);
const normalizedScript = hook.script.replace(/\\/g, '/');
const configRoot = path.resolve(context.filePath);
const resolvedPath = path.resolve(
context.filePath,
constants.HOOKS_DIRECTORY,
normalizedScript
);
if (!resolvedPath.startsWith(configRoot + path.sep)) {
if (context.config.AUTH0_ALLOW_EXTERNAL_CODE_PATHS) {
log.debug(
`Loading file outside config directory (AUTH0_ALLOW_EXTERNAL_CODE_PATHS enabled): "${hook.script}"`
);
} else {
log.warn(
`Path "${hook.script}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
`This will be blocked as an error in the next major release. ` +
`Move the file inside your config directory or set AUTH0_ALLOW_EXTERNAL_CODE_PATHS=true to allow it.`
);
}
}
hook.script = loadFileAndReplaceKeywords(resolvedPath, {
mappings: context.mappings,
disableKeywordReplacement: context.disableKeywordReplacement,
});
}

hook.name = hook.name.toLowerCase().replace(/\s/g, '-');
Expand Down
27 changes: 25 additions & 2 deletions src/context/directory/handlers/rules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';
import fs from 'fs-extra';
import { constants } from '../../../tools';
import { constants, loadFileAndReplaceKeywords } from '../../../tools';

import log from '../../../logger';
import { getFiles, existsMustBeDir, dumpJSON, loadJSON, sanitize } from '../../../utils';
Expand All @@ -25,7 +25,30 @@ function parse(context: DirectoryContext): ParsedRules {
}),
};
if (rule.script) {
rule.script = context.loadFile(rule.script, constants.RULES_DIRECTORY);
const normalizedScript = rule.script.replace(/\\/g, '/');
const configRoot = path.resolve(context.filePath);
const resolvedPath = path.resolve(
context.filePath,
constants.RULES_DIRECTORY,
normalizedScript
);
if (!resolvedPath.startsWith(configRoot + path.sep)) {
if (context.config.AUTH0_ALLOW_EXTERNAL_CODE_PATHS) {
log.debug(
`Loading file outside config directory (AUTH0_ALLOW_EXTERNAL_CODE_PATHS enabled): "${rule.script}"`
);
} else {
log.warn(
`Path "${rule.script}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
`This will be blocked as an error in the next major release. ` +
`Move the file inside your config directory or set AUTH0_ALLOW_EXTERNAL_CODE_PATHS=true to allow it.`
);
}
}
rule.script = loadFileAndReplaceKeywords(resolvedPath, {
mappings: context.mappings,
disableKeywordReplacement: context.disableKeywordReplacement,
});
}
return rule;
});
Expand Down
27 changes: 16 additions & 11 deletions src/context/directory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import pagedClient from '../../tools/auth0/client';
import cleanAssets from '../../readonly';
import log from '../../logger';
import handlers, { DirectoryHandler } from './handlers';
import { isDirectory, isFile, stripIdentifiers, toConfigFn } from '../../utils';
import { isDirectory, stripIdentifiers, toConfigFn } from '../../utils';
import { Assets, Auth0APIClient, Config, AssetTypes } from '../../types';
import { filterOnlyIncludedResourceTypes } from '..';
import { preserveKeywords } from '../../keywordPreservation';
Expand Down Expand Up @@ -47,16 +47,21 @@ export default class DirectoryContext {
}

loadFile(f: string, folder: string) {
const basePath = path.join(this.filePath, folder);
let toLoad = path.join(basePath, f);
if (!isFile(toLoad)) {
// try load not relative to yaml file
toLoad = f;
log.warn(
`Support for absolute paths and paths outside the config root will be deprecated in a future version to improve the security of the tool. ` +
`Please update your configuration to use paths relative to the config directory. ` +
`Current absolute path used: ["${f}"]`
);
const configRoot = path.resolve(this.filePath);
const basePath = path.resolve(this.filePath, folder);
const toLoad = path.resolve(basePath, f.replace(/\\/g, '/'));
if (!toLoad.startsWith(configRoot + path.sep)) {
if (this.config.AUTH0_ALLOW_EXTERNAL_CODE_PATHS) {
log.debug(
`Loading file outside config directory (AUTH0_ALLOW_EXTERNAL_CODE_PATHS enabled): "${f}"`
);
} else {
log.warn(
`Path "${f}" resolves to "${toLoad}" which is outside the config directory "${configRoot}". ` +
`This will be blocked as an error in the next major release. ` +
`Move the file inside your config directory or set AUTH0_ALLOW_EXTERNAL_CODE_PATHS=true to allow it.`
);
}
}
return loadFileAndReplaceKeywords(toLoad, {
mappings: this.mappings,
Expand Down
27 changes: 16 additions & 11 deletions src/context/yaml/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import pagedClient from '../../tools/auth0/client';

import log from '../../logger';
import { isFile, toConfigFn, stripIdentifiers, formatResults, recordsSorter } from '../../utils';
import { toConfigFn, stripIdentifiers, formatResults, recordsSorter } from '../../utils';
import handlers, { YAMLHandler } from './handlers';
import cleanAssets from '../../readonly';
import { Assets, Config, Auth0APIClient, AssetTypes, KeywordMappings } from '../../types';
Expand Down Expand Up @@ -58,17 +58,22 @@ export default class YAMLContext {
}

loadFile(f) {
let toLoad = path.join(this.basePath, f);
if (!isFile(toLoad)) {
// try load not relative to yaml file
toLoad = f;
log.warn(
`Support for absolute paths and paths outside the config root will be deprecated in a future version to improve the security of the tool. ` +
`Please update your configuration to use paths relative to the config directory. ` +
`Current absolute path used: ["${f}"]`
);
const configRoot = path.resolve(this.basePath);
const toLoad = path.resolve(this.basePath, f.replace(/\\/g, '/'));
if (!toLoad.startsWith(configRoot + path.sep)) {
if (this.config.AUTH0_ALLOW_EXTERNAL_CODE_PATHS) {
log.debug(
`Loading file outside config directory (AUTH0_ALLOW_EXTERNAL_CODE_PATHS enabled): "${f}"`
);
} else {
log.warn(
`Path "${f}" resolves to "${toLoad}" which is outside the config directory "${configRoot}". ` +
`This will be blocked as an error in the next major release. ` +
`Move the file inside your config directory or set AUTH0_ALLOW_EXTERNAL_CODE_PATHS=true to allow it.`
);
}
}
return loadFileAndReplaceKeywords(path.resolve(toLoad), {
return loadFileAndReplaceKeywords(toLoad, {
mappings: this.mappings,
disableKeywordReplacement: this.disableKeywordReplacement,
});
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export type Config = {
AUTH0_EXCLUDED_RESOURCE_SERVERS?: string[];
AUTH0_EXCLUDED_DEFAULTS?: string[];
AUTH0_EXPERIMENTAL_EA: boolean;
AUTH0_ALLOW_EXTERNAL_CODE_PATHS?: boolean;
}; // TODO: replace with a more accurate representation of the Config type

export type Asset = { [key: string]: any };
Expand Down
Loading