Skip to content

Validate the hash operator's salt type instead of crashing on len() - #2273

Open
feiiiiii5 wants to merge 1 commit into
data-privacy-stack:mainfrom
feiiiiii5:fix/hash-operator-salt-type-validation
Open

feiiiiii5 wants to merge 1 commit into
data-privacy-stack:mainfrom
feiiiiii5:fix/hash-operator-salt-type-validation

Conversation

@feiiiiii5

Copy link
Copy Markdown

What this fixes

The hash operator validates hash_type but never validates salt, so a salt of the wrong type survives validation and crashes inside operate():

TypeError: object of type 'int' has no len()

from presidio_anonymizer/operators/hash.py:41 (len(params[self.SALT])). Printed behaviour at f251c513, calling Hash.validate({"hash_type": "sha256", "salt": v}) directly:

  salt=123     -> TypeError (raw)
  salt=1.5     -> TypeError (raw)
  salt=True    -> TypeError (raw)
  salt=['x']   -> TypeError (raw)

This is not only an ugly exception type. Every sibling operator (replace, mask, encrypt) reports a bad parameter as InvalidParamError, and the anonymizer server turns that into a 4xx; a raw TypeError turns it into a 500 with no message for the caller. It also defeats the pre-flight check: validate() is the function the batch/EngineUtil path calls to reject bad configurations before any text is touched, and for hash it silently passed anything as long as hash_type was fine.

Change

The salt rules move into one _validate_salt() helper that both operate() and validate() call, so the two can no longer disagree:

  • non-str/bytes/bytearray salt → InvalidParamError("Invalid salt type '<type>'. ...")
  • str → encoded to bytes (unchanged)
  • empty → the existing "Salt parameter cannot be empty..." message, verbatim
  • shorter than 16 bytes → the existing "Salt must be at least 16 bytes (128 bits)..." message, verbatim
  • None / absent → random per-entity salt, as documented

Behaviour changes this PR declares (per AGENTS.md "Declare behavior changes")

  1. Hash.validate() now rejects a wrong-typed salt where it previously accepted it and let operate() blow up later. No previously-valid input becomes invalid.
  2. An explicit "salt": null used to raise TypeError: object of type 'NoneType' has no len(); it now means "no salt supplied", i.e. the documented random per-entity salt. null is how JSON expresses an absent value, so this matches the operator's own docstring instead of contradicting it.
  3. Error text for a wrong-typed salt is new; the two existing salt messages are untouched so anything matching on them still works.
  4. 16 is now the constant MIN_SALT_LENGTH instead of a literal in two places; same value, same semantics.

Tests

Added to presidio-anonymizer/tests/operators/test_hash.py: wrong-type salts (int, float, bool, list, object) through validate() and through anonymize(... OperatorConfig("hash", {"salt": ...})), salt: null producing a random salt, and the two existing length messages still raised for str and bytes inputs.

Base control — operators/hash.py restored from f251c513e8aad6820e9b2a44983ebf3a93637b07, with git diff -- <that file> printing nothing so the run measures upstream code with the new tests kept:

$ python -m pytest presidio-anonymizer/tests/operators/test_hash.py -q
7 failed, 22 passed
FAILED ...test_when_salt_is_not_a_string_or_bytes_then_validate_raises[1234567890123456 / 1.5 / True / bad_salt3 / bad_salt4]
FAILED ...test_when_salt_is_missing_or_none_then_random_salt_is_used
FAILED ...test_when_salt_type_is_invalid_then_anonymize_raises_invalid_param_error

With the patch applied: 29 passed. Whole package: presidio-anonymizer/tests323 passed (excluding test_ahds_surrogate.py, which needs the optional presidio-ahds extra that is not installed here; that file is untouched by this diff).

Lint: ruff check on the two changed files reports the same 29 pre-existing findings at base as at head — this diff adds none. Every changed line is executed by the tests above, which is what the AGENTS.md ">= 90% coverage on changed lines" rule asks for.

validate() only checked hash_type, so a non-string salt reached operate() and raised a raw TypeError: object of type 'int' has no len(). Over HTTP that is a 500 instead of the 422 every sibling operator returns, and the batch validator that calls validate() never saw the salt at all. Salt checks move into one helper used by both paths; an explicit salt: null now means the documented random salt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant