Let AES decrypt read both salt/IV normalizations, and annotate the md5 calls - #8299
Open
wintonzheng wants to merge 1 commit into
Open
Let AES decrypt read both salt/IV normalizations, and annotate the md5 calls#8299wintonzheng wants to merge 1 commit into
wintonzheng wants to merge 1 commit into
Conversation
…5 calls Converges this file with its counterpart in the private repo, which is the sync source. Decrypt now tries both the md5 and sha256 normalizations for the primary pair and every fallback, so ciphertext written by either normalization stays readable, and every md5 call is annotated usedforsecurity=False. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| def _encryption_params(salt: str | None, iv: str | None) -> tuple[bytes, bytes]: | ||
| return ( | ||
| hashlib.md5(salt.encode("utf-8"), usedforsecurity=False).digest() if salt else default_salt, | ||
| hashlib.md5(iv.encode("utf-8"), usedforsecurity=False).digest() if iv else default_iv, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket
No Linear ticket. Driven by an Oneleet code-security finding against
skyvern/forge/sdk/encrypt/aes.pyline 48 (high severity), plus its counterpart on line 50 of the sync source.Problem
The finding reports
Use of weak MD5 hash for security. Consider usedforsecurity=False. Twomd5()calls in this file are missing that annotation —default_ivand the fallback IV.The bigger issue is that this file has drifted from the repo it is synced from. This copy normalizes salt/IV with sha256 on the primary path and keeps md5 for decrypt fallbacks; the source copy still uses md5 for both. The sync is a whole-file copy, so the next upstream change that touches
aes.pyoverwrites this file — and the source copy has no sha256 decrypt candidates. Any self-hosted deployment that stored ciphertext under the sha256 normalization would silently lose the ability to decrypt it. Credentials and org auth tokens go through this path.That is not a hypothetical: it happens automatically on the next sync that touches this file.
Solution
Converge both copies on one file that reads every normalization ever written, so the sync becomes a no-op here instead of a data-loss event.
_decrypt_fallbacksbuilds candidates from both the md5 and sha256 normalizations, for the primary pair and every configured fallback, deduped and skipping the primary.md5()call is annotatedusedforsecurity=False, with a comment giving the accurate reason: every input is a fixed public constant or a 256-bit HMAC-SHA256 digest frombootstrap.py, so what makes a derived salt/IV unguessable is the secrecy of that input, not md5's collision resistance.Please read this part before approving. Converging means this copy's encrypt path moves from sha256 back to md5, matching the sync source. Nothing becomes unreadable — sha256 is now a decrypt candidate, so anything already stored still opens, and the rollback direction works too (verified below). But new ciphertext is written with md5-derived parameters again.
That is deliberate and temporary. The constraint is that the sync source cannot change its encrypt path yet: doing so would strand anything written between deploy and a rollback. Moving encrypt is only safe once every running instance can already read the target format, which is exactly what this change establishes. The follow-up flips both copies to sha256, and then to random per-message IVs in a versioned payload.
On the IV specifically: md5 is not the weakness there. The IV is deterministic — one per deployment rather than one per message — which costs AES-CBC its semantic security. Changing the hash does not fix that; random IVs do.
bootstrap.pyis intentionally untouched. This copy always inserts the primary pair into the decrypt fallbacks and the source copy only does so when salt and IV are unset; the new_decrypt_fallbackscovers the primary pair either way, so the difference no longer matters.How Has This Been Tested?
Both upgrade directions were simulated by loading this branch's module and current
main's side by side, with explicit salt and IV asbootstrap.pysupplies them:main(sha256) → readable by this branch.main, so a rollback is safe.Two tests added, both mutation-checked by reverting the mechanism singly:
test_decrypt_reads_sha256_normalized_ciphertexttest_encrypt_output_still_opens_with_md5_parameters_aloneThe existing
test_decrypts_ciphertext_created_with_legacy_primary_normalizationgolden ciphertext still passes unchanged, and has been added to the sync source's copy so a future sync stops stripping it.Run against the identical files in the source repo:
test_aes_fallback_decrypt.py,test_encrypt_bootstrap.py,test_secret_encryption.py— 29 passed;pytest tests/unit -k "encrypt or aes or secret or credential or token"— 2285 passed.ruff formatclean;ruff checkreports 4 pre-existing findings inencrypt/decryptthat are identical onmain.Artifacts (if appropriate):
Residual risk: more decrypt candidates means marginally more chances for strict PKCS#7 to accept wrong-key garbage on a decrypt that would otherwise fail outright. That tradeoff already exists in the multi-key loop; this widens it from 1 candidate to 2 per configured pair, and only on the failure path.