Skip to content

fix(encrypt): stop naive padding from truncating secrets ending in '*' - #43074

Merged
sadpandajoe merged 4 commits into
masterfrom
tdd/encrypted-field-trailing-asterisk-32664
Aug 13, 2026
Merged

fix(encrypt): stop naive padding from truncating secrets ending in '*'#43074
sadpandajoe merged 4 commits into
masterfrom
tdd/encrypted-field-trailing-asterisk-32664

Conversation

@rusackas

@rusackas rusackas commented Aug 11, 2026

Copy link
Copy Markdown
Member

SUMMARY

Fixes #32664 (reopened): any secret stored via the default AES-CBC EncryptedType field (this includes Database.password, and every other secret using the default encryption engine) silently loses a trailing * character the next time it's decrypted after being saved.

Root cause: the default field padded new writes with sqlalchemy_utils' "naive" padding scheme, which pads short plaintext with literal * bytes and unpads by unconditionally stripping every trailing * (NaivePadding.unpadvalue.rstrip(b"*"), no length or count encoded anywhere). If the real secret happens to end in *, that character is indistinguishable from padding and gets stripped along with it.

field = SQLAlchemyUtilsAdapter().create(SECRET, String(1024))
field.process_result_value(field.process_bind_param("mypassword*", DIALECT), DIALECT)
# used to be 'mypassword' -- now correctly 'mypassword*'

This is a different root cause than the original report, which was fixed by #30532 (a connection-string encoding issue at database-creation time, not this storage-layer corruption). The issue was closed on the assumption that fix covered it, but two independent users (@CamiloCarvajalPensemos and @ajunior) confirmed reproducing this on 6.1.0 well after the close, with @CamiloCarvajalPensemos providing the exact root-cause diagnosis this fix is built on.

The fix: BackwardCompatibleAesEngine, a drop-in AesEngine subclass that pads new writes with PKCS5 (the standard, self-validating padding scheme sqlalchemy_utils ships specifically as the safe alternative to naive padding) while still correctly decrypting values already stored under naive padding. decrypt tries PKCS5 unpad first — which raises InvalidPaddingError on anything that isn't validly PKCS5-padded — and falls back to naive unpad only when that fails.

This makes the fix safe to ship without a data migration: existing ciphertext is never rewritten in place, so already-stored secrets keep decrypting correctly, while every new write is safe going forward. ENCRYPTION_ENGINES["aes"] now resolves to this subclass, so the fix applies everywhere the default engine is used, not just Database.password.

TESTING INSTRUCTIONS

pytest tests/unit_tests/utils/encrypt_test.py tests/unit_tests/utils/test_encrypt_cbc_iv_reuse.py -v

22 tests pass, including:

  • the original repro (test_trailing_asterisk_survives_round_trip) and a multi-asterisk variant
  • test_legacy_naive_padded_secret_still_decrypts — the load-bearing proof that this needs no migration: encrypts a value with the raw upstream AesEngine (naive padding) directly, then decrypts it with BackwardCompatibleAesEngine, confirming it still reads back correctly
  • test_new_writes_use_pkcs5_padding — confirms new ciphertext actually uses the safer scheme
  • all pre-existing engine-migration / key-rotation tests, unaffected
  • updated one characterization test that pinned the exact previous engine class identity, now pinning BackwardCompatibleAesEngine with an explicit assertion that the deterministic-IV property it exists to test is unchanged (this class doesn't touch IV derivation, only padding)

Also ran the full tests/unit_tests/utils/ directory (782 passed, 1 pre-existing unrelated failure in a date-parsing test, nothing to do with this change).

ADDITIONAL INFORMATION

#32664 (reopened): the default AES-CBC EncryptedType field
(used for Database.password, among other secrets) relies on
sqlalchemy_utils' "naive" padding scheme, which pads short plaintext with
literal '*' bytes and unpads on decrypt by unconditionally stripping every
trailing '*' (NaivePadding.unpad -> value.rstrip(b"*")). A password or
token whose real value happens to end in '*' gets that character stripped
right along with the padding, silently corrupting the secret the next
time it's read back.

This is a different root cause than the original report (fixed by
#30532, which addressed the connection-string encoding used at
database-creation time). The issue was closed assuming that fix covered
it, but the underlying storage-layer corruption was never addressed;
two independent users confirmed reproducing it on 6.1.0 after the close.

Verified directly against Superset's own field factory
(SQLAlchemyUtilsAdapter, not just raw sqlalchemy_utils):
'mypassword*' round-trips through process_bind_param/process_result_value
as 'mypassword'.

Test-only: a real fix needs care. Naive padding is sqlalchemy_utils'
historical default (kept for backwards compatibility with existing
ciphertext), and switching padding schemes on an existing deployment has
the same re-encryption hazard already documented in
superset/utils/encrypt.py for the AES-CBC -> AES-GCM engine switch --
old encrypted values could become unreadable if the padding scheme just
changes under them without a migration path.

Fixes #32664
@rusackas rusackas added the tdd Test-first PR pinning a bug (see /maintenance TDD workflow) label Aug 11, 2026

@bito-code-review bito-code-review 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.

Code Review Agent Run #85c853

Actionable Suggestions - 1
  • tests/unit_tests/utils/encrypt_test.py - 1
Review Details
  • Files reviewed - 1 · Commit Range: 66d4119..66d4119
    • tests/unit_tests/utils/encrypt_test.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread tests/unit_tests/utils/encrypt_test.py
@rusackas
rusackas requested a review from sadpandajoe August 11, 2026 20:17
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.60870% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.61%. Comparing base (762fdcc) to head (08f4fd1).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
superset/utils/encrypt.py 86.36% 1 Missing and 2 partials ⚠️
superset/initialization/__init__.py 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #43074      +/-   ##
==========================================
- Coverage   66.64%   66.61%   -0.04%     
==========================================
  Files        2866     2866              
  Lines      162896   162808      -88     
  Branches    37525    37477      -48     
==========================================
- Hits       108568   108456     -112     
- Misses      52203    52224      +21     
- Partials     2125     2128       +3     
Flag Coverage Δ
hive 38.18% <65.21%> (+<0.01%) ⬆️
mysql 57.91% <82.60%> (+<0.01%) ⬆️
postgres 57.94% <82.60%> (+<0.01%) ⬆️
presto 40.13% <65.21%> (+<0.01%) ⬆️
python 59.34% <82.60%> (+<0.01%) ⬆️
sqlite 57.58% <82.60%> (+<0.01%) ⬆️
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Fixes the bug the previous commit's test pinned. The default AES-CBC
field padded new writes with sqlalchemy_utils' "naive" scheme, which pads
short plaintext with literal '*' bytes and unpads by unconditionally
stripping every trailing '*' -- silently eating a real trailing '*' in
any password/token right along with the padding.

Adds BackwardCompatibleAesEngine, a drop-in AesEngine subclass that pads
new writes with PKCS5 (the standard, self-validating scheme
sqlalchemy_utils ships for exactly this reason) while still decrypting
values already stored under naive padding: decrypt tries PKCS5 unpad
first -- which raises InvalidPaddingError on anything that isn't validly
PKCS5-padded -- and falls back to naive unpad only when that fails. That
makes the fix safe to ship with no data migration: existing ciphertext
is never rewritten, and old secrets keep decrypting correctly.

ENCRYPTION_ENGINES["aes"] now resolves to this subclass instead of the
raw upstream AesEngine, so it applies wherever the default engine is
used: Database.password and every other secret stored via the default
EncryptedType field, not just the specific case in the bug report.

Added regression coverage: multiple trailing asterisks, a legacy
naive-padded value read back correctly under the new engine (the
no-migration-needed property), and a check that new ciphertext is
actually PKCS5-padded. Updated one characterization test
(test_encrypt_cbc_iv_reuse.py) that pinned the exact previous engine
class identity -- it now pins BackwardCompatibleAesEngine instead, with
an assertion that the deterministic-IV behavior it exists to test is
unchanged (this engine doesn't touch IV derivation, only padding).

Fixes #32664
@pull-request-size pull-request-size Bot added size/L and removed size/S labels Aug 11, 2026
@rusackas rusackas changed the title test(encrypt): pin trailing-asterisk corruption in encrypted secrets fix(encrypt): stop naive padding from truncating secrets ending in '*' Aug 11, 2026
…dCompatibleAesEngine

check_encryption_engine() compared the resolved engine class against the raw
sqlalchemy_utils AesEngine, but ENCRYPTION_ENGINES["aes"] now resolves to
BackwardCompatibleAesEngine, so the legacy-engine startup warning silently
stopped firing. The key-rotation integration test decrypted under the
previous key with the default (NaivePadding) engine, which no longer matches
what values are actually written under -- fix it to decrypt with
BackwardCompatibleAesEngine, mirroring SecretsMigrator._source_decryptors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 08f4fd1
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a7dfa4e5cb11200083e25dc
😎 Deploy Preview https://deploy-preview-43074--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@bito-code-review

bito-code-review Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #525c0a

Actionable Suggestions - 0
Review Details
  • Files reviewed - 5 · Commit Range: 66d4119..3be3e88
    • superset/utils/encrypt.py
    • tests/unit_tests/utils/encrypt_test.py
    • tests/unit_tests/utils/test_encrypt_cbc_iv_reuse.py
    • superset/initialization/__init__.py
    • tests/integration_tests/utils/encrypt_tests.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread superset/utils/encrypt.py
"""

def _set_padding_mechanism(self, padding_mechanism: str | None = None) -> None:
super()._set_padding_mechanism("pkcs5")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This changes the stored CBC format in a way that old Superset processes cannot read: the previous AesEngine only removes *, so a value written here during a rolling upgrade or rollback is returned with its PKCS5 bytes appended and the password/token fails. Can we keep a write format old readers accept until this transition is coordinated?

@sha174n sha174n 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.

Reviewed the padding fix — PKCS5 with a naive-unpad fallback preserves backward-compat with existing stored values, and the cipher/key handling is unchanged. LGTM.

@sadpandajoe sadpandajoe added the merge-if-green If approved and tests are green, please go ahead and merge it for me label Aug 13, 2026
@sadpandajoe
sadpandajoe merged commit 3c7633f into master Aug 13, 2026
73 checks passed
@sadpandajoe
sadpandajoe deleted the tdd/encrypted-field-trailing-asterisk-32664 branch August 13, 2026 20:15
@bito-code-review

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped – PR Already Merged

Bito scheduled an automatic review for this pull request, but the review was skipped because this PR was merged before the review could be run.
No action is needed if you didn't intend to review it. To get a review, you can type /review in a comment and save it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-if-green If approved and tests are green, please go ahead and merge it for me preset-io size/L tdd Test-first PR pinning a bug (see /maintenance TDD workflow)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

special character in password of redshift like * cause create database failed

3 participants