Skip to content

Build1 publisher3 min readPublished

Cognito ships a new user pool with MFA off and its sign-in risk model disabled

A pool created with the defaults treats a password as the whole authentication factor and scores no risk on the sign-in. In the HackerOne chain a dev.to writeup walks through, either setting turned on would have stopped the takeover.

The Engineer · Build desk

Illustration accompanying Cognito ships a new user pool with MFA off and its sign-in risk model disabled

What happened

  • Creating a Cognito user pool through the console or with aws cognito-idp create-user-pool leaves MfaConfiguration set to OFF and UserPoolAddOns.AdvancedSecurityMode set to OFF.
  • Advanced Security scores each sign-in for unusual location, impossible travel and known compromised credentials, and can challenge, block or notify; switched off, none of that runs.
  • In the observation schema the post uses, the mfa_enforced verdict is true only when MfaConfiguration is ON and at least one second factor such as TOTP or SMS is enabled.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Any pool that aliases usernames to email without verifying an email change is one profile-update write away from a password reset that hands over the session.
  • decision A remediation ticket that says "set MfaConfiguration to ON" can close without changing anything the engine will accept, because the verdict also needs a factor enabled.
  • cost The detection that would catch the post-reset sign-in sits behind a paid add-on, so the operator who declines the add-on carries that class of takeover.
  • constraint A blanket MFA check cannot be the whole policy, since pools fronted by an SSO IdP need a per-pool opt-out to avoid firing on a legitimate configuration.

Sign-in against a pool in the default state runs one check: the submitted password against the stored credential. If it matches, Cognito issues the JWT the application trusts [2][3]. There is no second factor to demand, and no risk score to consult, because the model that would produce one belongs to the advanced-security add-on [6].

Cognito's per-user lockout counts failures against a single account [5]. An attacker who tries one password against each of thousands of known-valid usernames never gives any single account a second failure, so the threshold is never reached [5].

The account-takeover chain in bomma's HackerOne report, which the dev.to post walks through, starts upstream of both defaults. The pool used email as a username alias and did not require verification when a user changed that email [9]. Given a separate flaw that writes to the victim's profile email, the post's example is parameter pollution in a profile-update endpoint, the attacker moves the address to one they control and then runs the ordinary forgot-password flow [10]. With MFA on, the recovered password is one factor short of a session [11]. With advanced security on, the sign-in that follows the reset arrives from an unusual location and gets challenged for additional verification [12]. Either setting alone would have broken the chain at its last step, and both were off [15].

The observation schema in the post keeps the setting and the outcome apart. Its `mfa_enforced` field is true only when `MfaConfiguration` is ON and at least one second factor, TOTP or SMS, is enabled [13]. A team that flips the pool to ON and enables no factor has changed the configuration without changing the verdict [19].

The rule is narrower than "turn MFA on everywhere". Internal-only pools sometimes disable pool-level MFA legitimately because an upstream IdP enforces it, so the check fires by default and consumers opt out per pool through `scope_tags` on the asset [14]. "Customer-facing Cognito user pools must enforce MFA," the post writes [16].

The advanced-security leg is where the post's evidence thins. It calls the risk model "feature-flagged behind a paywall the operator chose not to pay" [18], and does not state a price for the add-on [20]. It also says MFA is the default that gets fixed quickly while advanced security frequently does not [7], and that both defaults are how the bomma report "and a long tail of similar disclosures end up shipped to production" [8]. Those are offered as the author's experience, with no population or count behind them.

Both boxes appear on the same console screen. They get treated as one decision, and then handled as one. The control that catches the full shape, alias plus unverified email change plus single factor, is identified in the post as `CTL.COGNITO.INCOMPLETE.001` [17].

What to watch

  • Whether AWS changes the create-user-pool default for either setting, or repackages the advanced-security add-on.
  • Any published count of live pools running with advanced security off, which would move the persistence claim from one author's experience to measurement.
  • Whether the scope_tags opt-out on CTL.COGNITO.INCOMPLETE.001 starts getting applied to customer-facing pools to silence the check.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories