Repair the Reset Link Boundary

CybersecurityBeginner
Practice Now

Introduction

Harbor's member portal lets a person recover an account after forgetting a password. A reset email carries a temporary token: possession of that token should authorize one password change for one account. Support has found that links can be reused and can change another account's password.

You will first recover a synthetic account normally, observe the unsafe behavior, and repair one small Python handler. No Python experience is required; the lesson explains the function and conditions before the edit. You should be comfortable entering a terminal command and switching tabs.

Use Portal Web for the application, Mailbox Web for synthetic email, and App Server for the prepared code. They run on this one independent training instance. There is no external email delivery. Tokens, expiry comparisons, database transactions, password changes, and login are real; this small app is not a production identity system.

Recover an Account Normally

In this step, you will follow a reset email and sign in with the new password, establishing the business behavior the repair must preserve.

An account identifies a person; a password proves that person can sign in. If a password is forgotten, a reset link temporarily authorizes changing it. The token inside the link is not the password. This local mailbox lets you act as the owner of two synthetic accounts, maya and noah. Both begin with StartPass42!. Only use these training accounts.

In Portal Web, find Request a reset link. Enter maya in Recovery account and click Send reset link. The response says Link requested. Open Local Mailbox. Refresh Mailbox Web to see the newest message at the top. It names the recipient and contains Open reset link plus a token string.

Click Open reset link in that message. The link opens a reset form within the same tab. It fills Token and Reset account. Keep those fields unchanged; enter FirstPass42! in New password, then click Reset password. Expect Reset accepted and HTTP 200.

HTTP is the request/response conversation between a browser and a server. Here 200 means accepted, 403 means a valid request was denied, and 500 means an application error. A connection error is different from a security denial.

Open Portal Web, refresh if needed, and use Sign in: Login account maya, Password FirstPass42!, then Sign in. Expect Login accepted. A working reset changes the actual password, not just a label on a page.

If you lose a link, request another through the same form and use the newest mailbox message. Links last 30 minutes. You do not need to restart the environment for an expired link or typing mistake.

Observe the Broken Boundary

In this step, you will use disposable demonstration changes to distinguish a token's owner, lifetime, and one-time permission.

A browser sends form fields, each with a name and value. A user can change them, so a server cannot trust Reset account merely because the original email filled it. The server holds a separate token record:

Field Meaning
account Account allowed to use this token
expires_at Last allowed time, stored as seconds since a fixed clock origin
used 0 before use; 1 after use

A new token is usable only for its owner before expiry. A successful reset moves it to used; it must never become usable again. An unused token can also become expired as time passes. Neither a used nor an expired token authorizes another reset.

Request a new link for maya in Portal Web. Refresh Mailbox Web, open the newest link, and reset maya to DemoPass42!. Now reopen the same message's link and reset maya to ReplayDemo42!. Both requests say Reset accepted. The second should have been denied: this is replay, reusing permission that should be exhausted.

Reopen that same link again. Change Reset account to noah and New password to CrossDemo42!; click Reset password. It is accepted. In Portal Web, sign in as noah with CrossDemo42!. A link issued for maya changed noah: the server failed to bind the token to its account.

Finally return to the reset form in Portal Web. Enter expired-demo in Token, maya in Reset account, and ExpiredDemo42! in New password. This prepared token expired an hour before setup. Click Reset password. It is also accepted: the server ignored the clock. You do not have to wait for a timer to observe the error.

These accounts are disposable, and the next repair tests use clean account copies. Keep the observed account state; no manual database editing is needed.

Bind and Consume the Token

In this step, you will repair the small server function and test it without changing the live demonstration accounts.

Switch to App Server. It starts in /home/labex/project/reset. cat displays a text file; the filename is its argument. Read the handler:

cat handler.py

The first line imports two prepared helpers. def defines a function; names inside parentheses are its parameters, the inputs for one call. eligible(row, account, now) receives the stored token record, the submitted account, and the current server time. row["account"] reads one field from the stored record. row is None means no token was found. The indented if body runs only when that condition holds; return False denies permission. return True grants it. Four leading spaces define a block in Python.

The current function returns True for every existing token, explaining the three failures. The reset function also calls unsafe_reset, which changes a password without consuming the token.

The repair needs all conditions together. == compares values, > means greater than, and and requires both sides to be true. or would accept either side and let one valid property hide another invalid property. A future expiry is greater than now; equality is already expired. used == 0 permits only an unused token.

The supplied consume_and_reset helper starts a SQLite transaction, reads the current token, calls your eligible function, and commits the password change and used = 1 together. This is atomicity: those changes succeed together. It locks before reading, so two competing requests cannot both see an unused token. You do not implement the locking or password hashing.

Open the small file with nano, a terminal text editor:

nano handler.py

Replace the file's contents with the following code. You can use the arrow keys and Backspace/Delete to remove old text, then paste the complete replacement. Keep spaces and punctuation exactly; the parentheses allow the long condition to span several lines.

from framework import unsafe_reset, consume_and_reset


def eligible(row, account, now):
    if row is None:
        return False
    return (row["account"] == account
            and row["expires_at"] > now
            and row["used"] == 0)


def reset(db, token, account, password):
    return consume_and_reset(db, token, account, password, eligible)

Save with Ctrl+O, press Enter to confirm handler.py, then exit with Ctrl+X. Each Web request reads the saved handler, so no service restart is required. If you see HTTP 500, reopen the file and compare indentation, brackets, and quotes.

python3 runs a Python program. The prepared checker takes your handler path and starts a separate HTTP application against temporary SQLite fixtures. It tests the actual handler without fixing it or changing the live accounts:

python3 /opt/reset-lab/check.py handler.py

Expect eleven PASS lines, including valid reset accepted, replay denied, and competing use has exactly one winner. For an unsafe edit, FAIL names the broken property. Denying every request fails the legitimate reset and login checks. Editing browser fields or the live database cannot fix the handler tested against fresh fixtures. Reopen nano handler.py, correct the saved code, and rerun the same checker as needed.

Prove Recovery Still Works

In this step, you will exercise the repaired live service and connect its outcomes with the isolated security checks.

In Portal Web, request a fresh link for maya. Refresh Mailbox Web, open the newest link, and reset maya to ResetSafe42!. Expect Reset accepted and HTTP 200. Reopen that same message's link and try ReplayAgain42! for maya. Expect Reset denied and HTTP 403.

Request another fresh maya link. Open it, change Reset account to noah, and try BlockedCross42!. Expect Reset denied. That denial should not consume the owner's unused link: reopen the same message, keep maya as Reset account, and set ResetSafe42!. Expect success.

In the reset form, try Token expired-demo, Reset account maya, and New password BlockedExpired42!. Expect Reset denied. Then in Portal Web, sign in as maya with ResetSafe42!. It must say Login accepted: failed attempts did not change the password. You may also sign in as noah with its demonstration password CrossDemo42!; a repaired endpoint preserves existing unrelated accounts.

In App Server, rerun the disposable regression checks:

python3 /opt/reset-lab/check.py handler.py

All eleven checks should still pass, including two simultaneous uses with exactly one successful reset. Repeating this checker leaves live account records unchanged. This combines permitted recovery with denied misuse; a disabled reset page or an always-deny handler is not a successful repair.

The teaching app uses synthetic delivery and simplified identity handling. Production recovery also needs controls such as secure email delivery, rate limits, session handling, and password policy; those are outside this focused repair. OWASP's reset guidance provides the broader context.

Summary

You followed a real password reset, demonstrated replay, cross-account and expiry failures, then repaired a small Python handler. Stored ownership, a future expiry, unused state, and atomic consumption now work together. Disposable HTTP tests prove valid recovery, denied misuse, unchanged unrelated accounts, and exactly one winner under competing use. The staff portal challenge asks you to apply these same rules to a different incomplete condition.