Skip to content

[DXAA-651] ProdSec updates to onboarding tool#179

Merged
wdaimee merged 45 commits into
mainfrom
fix-security-issues
Jul 3, 2026
Merged

[DXAA-651] ProdSec updates to onboarding tool#179
wdaimee merged 45 commits into
mainfrom
fix-security-issues

Conversation

@mchang16-auth

@mchang16-auth mchang16-auth commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Changes

Write guard & audit log (env-credentials.ts)

  • Added a 30 second write guard that prevents accidental double-writes to the same project. Overlapping writes are blocked with a descriptive error.
  • Added an append-only audit log (.auth0-mcp-writes.log) recording each write with timestamp, framework, keys written, and target file.
  • Both the write guard state file and audit log are automatically added to .gitignore.

Dry-run mode (env-credentials.ts, applications.ts)

  • New dry_run parameter returns the computed key names without writing to disk, so callers can confirm what would be written before committing.

Path validation hardening (env-credentials.ts, quickstarts.ts)

  • Replaced the loose absolute-path check with validateProjectPath(), which enforces that the resolved path stays within the current working directory and contains no .. traversal sequences.
  • Added Zod schema validation on envSnippet.fileName to reject paths with directory components at the CDN spec layer.
  • Validated envSnippet.fileName again at use time before constructing the env file path.
  • Error message for path traversal no longer reflects back user-supplied paths.

Atomic writes & better parsing (credentials-writer.ts)

  • .env files are now written atomically via a temp file + rename(), preventing a half-written file on crash.
  • Replaced the hand-rolled env parser with the dotenv library for correct handling of quoted values, escapes, and edge cases.
  • ensureGitignore now returns a boolean and avoids duplicating the # Auth0 credentials comment header.
  • Extracted CREDENTIAL_FILE_MODE = 0o600 as a non-overridable constant; on POSIX, throws if the post-write stat mode doesn't match (previously chmod failures were silently swallowed)
  • Rejects existing .env files exceeding 1 MB or that are not regular files (directories, symlinks, pipes) before reading; same guard applied to parseEnvFile

Rate limiting

  • Enforces a 5-calls-per-60-second sliding window on auth0_save_credentials_to_file at the server dispatch level; returns a clear retry-after error on excess calls; other tools are unaffected

CDN error handling (env-credentials.ts)

  • Supported frameworks that return a null spec (CDN failure, no cache) now return a clear network error instead of silently falling back to generic variable names.

Env file write safeguards (src/utils/env-credentials.ts)

  • Detects when project_path looks like a web server document root (e.g. /var/www/..., or a directory named public, dist, build, static, etc.) and appends a warning to the success message advising to block .env file access in the web server config
  • Has a deny list to deny writing of .env if in a system or home directory, or sensitive home directory (ex. ~/.ssh)

Schema (src/tools/applications.ts, src/tools/onboarding.ts, src/tools/quickstarts.ts)

  • Added additionalProperties: false to the input schemas for auth0_save_credentials_to_file, auth0_onboarding, and auth0_configure_and_get_quickstart_guide so safe guards from injection attacks and compliant MCP clients will reject calls with undocumented parameters (e.g. permissions: "640")

References

https://auth0team.atlassian.net/browse/DXAA-651
https://oktainc.atlassian.net/wiki/spaces/REX/pages/927957893/Security+Review+Report+-+PSREV-2889+Auth0+MCP+Server+auth0_save_credentials_to_file+tool+Updates

Testing

New and expanded tests cover write guard logic, dry-run, atomic writes, path traversal rejection, .gitignore deduplication, parseEnvFile edge cases, and the CDN failure path.

  • This change adds unit test coverage
  • This change adds integration test coverage

Checklist

GilbertLS and others added 22 commits June 8, 2026 11:22
Remove the duplicate hasNonVerifiableCallbacks from quickstart-guide now
that the canonical implementation from PR #162 is available, resolving the
standing TODO. Drops the redundant test block; coverage lives in
onboarding.test.ts.
Replace the hardcoded VALID_FRAMEWORKS list with the shared
SUPPORTED_FRAMEWORKS/isFrameworkSupported from onboarding util.
This also makes framework validation case-insensitive, consistent
with fetchQuickstartSpec which already lowercases the framework.
SUPPORTED_FRAMEWORKS derives order from FRAMEWORK_FILENAMES
(react, vue, angular, nextjs); update assertion to match.
A relative project_path resolved against the MCP server's stdio subprocess
cwd (the client's launch dir), never the user's project — silently reading
the wrong .env. Reject non-absolute paths at the handler boundary in both
tools sharing the project_path contract, and tighten the schema description.
The response reported the raw project_path while all file detection used the
resolved path, so a normalizable input (e.g. trailing /sub/..) could point
somewhere different from where files were actually inspected. Return the
normalized resolvedProjectPath so the response matches the work done.
URL updates are strictly additive ([...existing, ...missing]) — nothing is
removed or overwritten — so destructiveHint: true overstated the tool's
behavior. Flip it to false and, matching the sibling app tools, instruct the
LLM to inform the user about auto-applied settings like
skip_non_verifiable_callback_uri_confirmation_prompt.
When no environment file is detected, the error now enumerates the filenames
detectExistingEnvFile scans (.env.local, .env, .env.development.local,
.env.development) plus the spec's snippet file, so the LLM/user knows exactly
what to create instead of guessing.
The flag was only nested under configured_urls, but the per-response
instructions only tell the LLM to summarize actions_taken — so the
auto-applied setting was never reported to the user. Push an explicit line
into actions_taken when the flag is set, riding the channel the instructions
already cover.
Clarify that an existing environment file was detected (not assumed to hold
Auth0 credentials), keeping the instruction to use it as-is and skip .env
setup steps in the prompt.
The old name auth0_get_quickstart_guide implied a read-only fetch, but the
tool also configures callback/logout/web-origin URLs on the Auth0 app. Rename
it so the name reflects that it both configures the application and returns
the quickstart guide.
Orchestrates Auth0 project onboarding by validating framework/path,
fetching CDN spec, creating an application with correct config, and
saving credentials to .env. Returns next_steps for quickstart guide.
When save_credentials fails, include the created client_id in the error
so the LLM knows the app exists. Add defensive defaults for saveData
fields. Add tests for path resolution and case-insensitive framework.
…the onboarding tool for unsupported frameworks
…x .env file detection when running the get quickstart tool
…e before and after the save credentials tool execution
@mchang16-auth mchang16-auth changed the title addressing security concerns + tests [DXAA-651] ProdSec updates to onboarding tool Jun 11, 2026
Comment thread src/utils/credentials-writer.ts
Comment thread src/utils/credentials-writer.ts
return 'project_path does not exist or is not a directory';
}
return null;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here on line 64, path.resolve() turns the path into an absolute path. We then check it to make sure exists and that it's within the current working directory

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the tool definitions, we are stating that the project path is an absolute path. I am okay doing this and double-checking, however, we do kind lose the preventative behaviour of stopping the user from providing a relative path. I am okay either way, but it's a bit inconsistent with the schema definitions:

  • applications.ts:365 — 'Absolute path to the project root directory. The env file is written here.'
    • quickstarts.ts:45 — 'Absolute path to the project directory. Used for .env file check and project config port
      detection.'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wdaimee I added back the isAbsolute() check since like what you mentioned and what @kushalshit27 the check is needed


expect(parseEnvFile(envFilePath)).toEqual({ FOO: 'bar' });
});
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two test cases are due to the change from regex to dotenv library usage. Using dotenv will strip surrounding quotes and trims inline comments from unquoted values. Previously the regex would miss these cases

@mchang16-auth
mchang16-auth marked this pull request as ready for review June 12, 2026 21:17
@mchang16-auth
mchang16-auth requested a review from a team as a code owner June 12, 2026 21:17
});

describe('resolveAndWriteCredentials — project path validation', () => {
it('returns error when project_path is not absolute', async () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this test case since we are not using isAbsolute() anymore (path.resolve() makes the path absolute within validateProjectPath())

Comment thread src/utils/credentials-writer.ts Outdated
() => commentOutConflictingKeys(existingContent, incomingKeys),
ENV_PARSE_TIMEOUT_MS,
'Env file processing'
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we use a timeout to ensure when parsing the env file it does not catch when reading a potentially large file (which should not be realistic anyway for a env file)

throw new Error(
`Security error: env file exceeds maximum allowed size of ${MAX_ENV_FILE_SIZE_BYTES / 1024 / 1024} MB. ` +
'The file may be corrupted or malicious. Remove or truncate it before retrying.'
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers https://auth0team.atlassian.net/browse/SEC-32494 to prevent a large .env file causing the MCP to hang

throw new Error(
`Security error: expected file mode ${CREDENTIAL_FILE_MODE.toString(8)}, got ${actualMode.toString(8)}`
);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers https://auth0team.atlassian.net/browse/SEC-32495 and prevents access if the file has a non 600 permission

root => resolvedDir === root || resolvedDir.startsWith(root + path.sep)
);
}

@mchang16-auth mchang16-auth Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function covers https://auth0team.atlassian.net/browse/SEC-32459 and warns the user to double check their .env file is not made public on the web server. The method is called in src/utils/env-credentials.ts

Comment thread src/server.ts
);
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method covers https://auth0team.atlassian.net/browse/SEC-32517 and imposes a sliding window rate limiting (checks the last 60 secs for calls made)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the rate limiter apply to dry_run calls at the server layer, before the handler knows the call is non-writing?

@mchang16-auth mchang16-auth Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dry_run would get rate limited, which I don't think we should have since no writes are actually happening. I've gone and removed the rate limited if dry_run is true (line 119)

Comment thread src/utils/credentials-writer.ts
Comment thread src/utils/rate-limiter.ts
calls.set(key, timestamps);
return { allowed: true, retryAfterMs: 0 };
};
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createRateLimiter(5, 60_000) is called once at startup and returns a function. Each time you call that function with a key (e.g. 'auth0_save_credentials_to_file'), it either allows or denies the call:

  • It keeps a list of timestamps for each key, pruning any older than 60 seconds on every check
  • If fewer than 5 timestamps remain after pruning, the call is allowed and the current timestamp is recorded
  • If 5 or more remain, the call is denied and retryAfterMs tells the caller how long until the oldest timestamp expires out of the window

Comment thread src/utils/credentials-writer.ts Outdated
Comment thread src/utils/env-credentials.ts Outdated
Comment thread src/utils/env-credentials.ts Outdated
Comment thread src/utils/env-credentials.ts Outdated
wdaimee
wdaimee previously approved these changes Jun 25, 2026

@wdaimee wdaimee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

return WEB_SERVED_SEGMENT_NAMES.has(path.basename(resolvedDir).toLowerCase());
}

function validateProjectPath(projectPath: string): string | null {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the path.isAbsolute() check got dropped here. The new traversal check only catches paths within them. It prevents the resolved directory from changing depending on the server's CWD at call time, which matters in containers or multi-user setups.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kushalshit27 thanks for pointing that out, I've gone ahead and added the isAbsolute() check back in, along with some refactoring that comes with path.resolve() not being needed then anymore

@kushalshit27 kushalshit27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@wdaimee wdaimee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@wdaimee
wdaimee merged commit ef72885 into main Jul 3, 2026
2 checks passed
@wdaimee
wdaimee deleted the fix-security-issues branch July 3, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants