Skip to content

fix!: prevent plaintext API key storage via setup wizard - #97

Merged
CybotTM merged 2 commits into
mainfrom
fix/setup-wizard-plaintext-api-key
Mar 6, 2026
Merged

fix!: prevent plaintext API key storage via setup wizard#97
CybotTM merged 2 commits into
mainfrom
fix/setup-wizard-plaintext-api-key

Conversation

@CybotTM

@CybotTM CybotTM commented Mar 6, 2026

Copy link
Copy Markdown
Member

Summary

Security fix: The Setup Wizard stored raw API keys in plaintext in the database, bypassing nr_vault encryption entirely. This was caused by using Extbase persistence ($this->providerRepository->add()) which does not trigger TYPO3 DataHandler hooks where nr_vault intercepts writes.

Security impact

  • Raw API keys (e.g., sk-proj-...) stored as plaintext in tx_nrllm_provider.api_key
  • Accessible to anyone with database read access, visible in backups, logs, List module
  • Provider was also non-functional: getDecryptedApiKey() tried to use raw key as vault identifier → SecretNotFoundException → returned empty string

Changes

SetupWizardController.php (P0 Critical)

  • Added VaultServiceInterface as constructor dependency
  • saveAction() now stores the API key in vault via $this->vaultService->store() before Extbase persistence
  • Only the vault UUID is saved to the database
  • Returns HTTP 500 JSON error if vault storage fails (prevents silent fallback to plaintext)
  • Added generateVaultIdentifier() method producing UUID v7 format

Provider.php (P1 Defense-in-depth)

  • setApiKey() now validates that the value is a UUID v7 vault identifier or empty string
  • Throws InvalidArgumentException if a raw secret is passed (defense-in-depth)
  • Added isVaultIdentifier() private static method for UUID v7 format validation
  • getDecryptedApiKey() detects legacy plaintext values and emits E_USER_WARNING instead of silently failing vault lookups

Tests (8 files updated)

  • Updated test fixtures to use UUID v7 format strings instead of raw API keys
  • Added tests for raw key rejection in Provider model
  • Updated fuzzy/security tests to verify injection attempts are rejected

Breaking Change

Provider::setApiKey() now rejects raw API keys. Code that previously set raw keys must:

  1. Use VaultServiceInterface::store($identifier, $secret, $metadata) first
  2. Pass the resulting UUID to setApiKey()

Remediation for existing data

Use the vault CLI to migrate existing plaintext values:

vendor/bin/typo3 vault:migrate-field tx_nrllm_provider api_key

Test plan

  • Unit tests: 2706 tests pass (20777 assertions)
  • PHPStan: 0 errors
  • php-cs-fixer: 0 issues
  • GrumPHP pre-commit: all 6 tasks pass
  • Verify setup wizard stores API key as vault UUID, not plaintext
  • Verify provider test connection works after wizard setup
  • Verify vault:migrate-field remediates existing plaintext values

BREAKING CHANGE: Provider::setApiKey() now rejects raw API keys and
only accepts vault identifiers (UUID v7 format) or empty strings.
Code that previously set raw API keys must use VaultServiceInterface
to store the secret first and pass the resulting UUID.

The SetupWizardController stored API keys via Extbase persistence
($this->providerRepository->add()), which completely bypasses
TYPO3 DataHandler hooks where nr_vault intercepts writes to store
secrets securely. This resulted in raw API keys (e.g., sk-proj-...)
being stored as plaintext in the tx_nrllm_provider.api_key column.

Security impact: plaintext API keys are accessible to anyone with
database read access, visible in backups, logs, and the TYPO3 List
module. The provider was also non-functional since getDecryptedApiKey()
tried to use the raw key as a vault identifier.

Fixes:
- SetupWizardController now uses VaultServiceInterface to store the
  API key before Extbase persistence, saving only the vault UUID
- Provider::setApiKey() validates UUID format, rejecting raw secrets
  as defense-in-depth
- Provider::getDecryptedApiKey() detects legacy plaintext values and
  emits E_USER_WARNING instead of silently failing vault lookups
- Returns HTTP 500 if vault storage fails, preventing silent fallback
  to plaintext

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Copilot AI review requested due to automatic review settings March 6, 2026 10:14
@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for your first pull request! We are excited to have you contribute.
A maintainer will review your PR soon. Please ensure all CI checks pass.

@github-actions github-actions Bot added tests Test-related changes backend TYPO3 backend changes labels Mar 6, 2026
@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

github-actions[bot]
github-actions Bot previously approved these changes Mar 6, 2026

@github-actions github-actions Bot 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.

Automated approval for solo maintainer project

This PR has passed all automated quality gates:

  • ✅ Static analysis (PHPStan)
  • ✅ Code style (PHP-CS-Fixer)
  • ✅ Unit & functional tests
  • ✅ Security scanning
  • ✅ Dependency review

See SECURITY_CONTROLS.md for compensating controls documentation.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical security vulnerability where API keys were stored in plaintext in the database by the Setup Wizard, bypassing existing encryption mechanisms. The changes ensure that all API keys are now securely stored in a vault, with only an encrypted reference (UUID v7) persisted in the database. This significantly enhances data security by preventing direct exposure of sensitive credentials and includes validation to prevent future plaintext storage, along with mechanisms to handle and migrate existing legacy data.

Highlights

  • Security Fix: Plaintext API Key Storage: The Setup Wizard no longer stores raw API keys directly in the database. Instead, it now securely stores them in a vault, and only a UUID reference is saved to the database, preventing plaintext exposure.
  • Enhanced API Key Validation: The Provider model's setApiKey() method now strictly enforces that API keys must be valid UUID v7 vault identifiers or an empty string, throwing an InvalidArgumentException if a raw secret is passed, adding a defense-in-depth layer.
  • Legacy Data Handling and Migration: The getDecryptedApiKey() method now detects legacy plaintext API keys, logs a security warning, and returns an empty string to prevent accidental use of unencrypted secrets. A CLI command is provided for migrating existing plaintext values to the vault.
  • Comprehensive Test Updates: All relevant tests, including E2E, functional, fuzzy, and unit tests, have been updated to reflect the new UUID v7 format for API keys and to verify the rejection of raw API keys, ensuring the robustness of the security enhancements.
Changelog
  • Classes/Controller/Backend/SetupWizardController.php
    • Added VaultServiceInterface as a constructor dependency to enable secure API key storage.
    • Modified saveAction() to store API keys in the vault via vaultService->store() and save the resulting vault UUID to the database.
    • Implemented error handling in saveAction() to return an HTTP 500 JSON error if vault storage fails.
    • Introduced generateVaultIdentifier() method to create UUID v7 format identifiers for the vault.
  • Classes/Domain/Model/Provider.php
    • Added InvalidArgumentException import for new validation logic.
    • Updated getDecryptedApiKey() to detect legacy plaintext API keys, trigger an E_USER_WARNING, and return an empty string.
    • Modified setApiKey() to validate that the input is either an empty string or a UUID v7 vault identifier, throwing an InvalidArgumentException for raw secrets.
    • Added a private static helper method isVaultIdentifier() to validate UUID v7 format.
  • Tests/E2E/Backend/ErrorPathwaysE2ETest.php
    • Imported InvalidArgumentException for new test assertions.
    • Updated all API key fixtures to use UUID v7 format strings instead of raw API keys.
    • Replaced the pathway7_5_veryLongApiKey_handledSafely test with pathway7_5_rawApiKey_rejectedByValidation to assert that raw API keys are rejected by setApiKey().
  • Tests/E2E/Backend/MultiProviderWorkflowsE2ETest.php
    • Updated API key fixtures to use UUID v7 format strings.
  • Tests/E2E/Backend/ProviderManagementE2ETest.php
    • Updated API key fixtures to use UUID v7 format strings.
  • Tests/Functional/Fixtures/Providers.csv
    • Updated the api_key for the 'openai-test' provider to a UUID v7 format.
  • Tests/Functional/Repository/ProviderRepositoryTest.php
    • Updated the API key fixture to use a UUID v7 format string.
  • Tests/Fuzzy/Domain/ProviderFuzzyTest.php
    • Imported InvalidArgumentException for new test cases.
    • Added apiKeyAcceptsVaultIdentifiersAndEmptyString() test to verify acceptance of valid UUID v7 and empty strings.
    • Added apiKeyRejectsRawSecrets() test to ensure setApiKey() throws an InvalidArgumentException for non-UUID API keys.
  • Tests/Fuzzy/Security/InputSanitizationFuzzyTest.php
    • Imported InvalidArgumentException for new test assertions.
    • Renamed providerHandlesPotentialInjectionInApiKey() to providerRejectsRawApiKeysIncludingInjectionAttempts().
    • Modified the test to assert that setApiKey() throws an InvalidArgumentException when raw API keys (including injection attempts) are provided.
  • Tests/Unit/Controller/Backend/ModelControllerTest.php
    • Updated the API key fixture to use a UUID v7 format string.
Activity
  • Unit tests have been executed, with 2706 tests passing and 20777 assertions.
  • PHPStan analysis reported 0 errors, indicating code quality and type safety.
  • php-cs-fixer found 0 issues, ensuring adherence to coding standards.
  • GrumPHP pre-commit hooks passed all 6 tasks, verifying code integrity before commit.
  • Manual verification is pending for the setup wizard's secure API key storage, provider test connection, and the vault:migrate-field remediation tool.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This is an excellent security fix that correctly addresses the plaintext storage of API keys by integrating with nr_vault. The defense-in-depth approach in the Provider model, which now validates for a vault identifier format, is a great addition to prevent accidental misuse. The test suite has also been thoroughly updated to reflect these critical changes.

I have one suggestion to improve maintainability by replacing the manual UUID v7 generation with a call to a standard library component, which is detailed in the specific comment.

Comment thread Classes/Controller/Backend/SetupWizardController.php Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Security-focused change to ensure provider API keys are never persisted as plaintext by the Setup Wizard, aligning wizard behavior with nr-vault’s encrypted secret storage model.

Changes:

  • Setup Wizard now stores the raw API key in nr-vault first and persists only the vault identifier.
  • Provider model hardens against plaintext secrets by validating apiKey as a vault identifier and warning on legacy plaintext values.
  • Updates unit/functional/fuzzy/E2E tests + fixtures to use vault-identifier-shaped values and assert rejection of raw secrets.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Classes/Controller/Backend/SetupWizardController.php Stores API key via VaultServiceInterface and saves only a generated vault identifier
Classes/Domain/Model/Provider.php Rejects non-vault identifiers in setApiKey() and warns on legacy plaintext in getDecryptedApiKey()
Tests/Unit/Controller/Backend/ModelControllerTest.php Updates provider fixture API key to UUID v7-shaped identifier
Tests/Fuzzy/Security/InputSanitizationFuzzyTest.php Changes expectation to reject raw/injection-like API keys
Tests/Fuzzy/Domain/ProviderFuzzyTest.php Adds acceptance test for UUID v7/empty and rejection property test for raw secrets
Tests/Functional/Repository/ProviderRepositoryTest.php Updates persisted provider API key to UUID v7-shaped identifier
Tests/Functional/Fixtures/Providers.csv Updates fixture data to store UUID v7-shaped identifier instead of plaintext
Tests/E2E/Backend/ProviderManagementE2ETest.php Updates E2E setup flows to use UUID v7-shaped identifiers
Tests/E2E/Backend/MultiProviderWorkflowsE2ETest.php Updates multi-provider E2E fixtures to use UUID v7-shaped identifiers
Tests/E2E/Backend/ErrorPathwaysE2ETest.php Updates error-path E2E fixtures and adds assertion that raw keys are rejected

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Classes/Domain/Model/Provider.php
Comment thread Classes/Controller/Backend/SetupWizardController.php Outdated
Comment thread Classes/Controller/Backend/SetupWizardController.php Outdated
Comment thread Classes/Domain/Model/Provider.php
Replace the manual UUID v7 implementation in SetupWizardController
with Symfony\Component\Uid\Uuid::v7()->toRfc4122() as suggested
in code review. The symfony/uid component is available as a
transitive dependency through TYPO3.

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>

@github-actions github-actions Bot 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.

Automated approval for solo maintainer project

This PR has passed all automated quality gates:

  • ✅ Static analysis (PHPStan)
  • ✅ Code style (PHP-CS-Fixer)
  • ✅ Unit & functional tests
  • ✅ Security scanning
  • ✅ Dependency review

See SECURITY_CONTROLS.md for compensating controls documentation.

@CybotTM
CybotTM added this pull request to the merge queue Mar 6, 2026
Merged via the queue into main with commit ed2b7ec Mar 6, 2026
39 checks passed
@CybotTM
CybotTM deleted the fix/setup-wizard-plaintext-api-key branch March 6, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend TYPO3 backend changes tests Test-related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants