- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Harden a Java Web Application Against OWASP Risks
This internal portal allows employees to upload, tag, and download company documents. A recent security review identified several vulnerabilities aligned with the OWASP Top 10: filenames and comments are trusted without validation or encoding, passwords are compared in plaintext, session IDs are predictable, and login requests are accepted over HTTP. Additionally, JWTs are accepted without verifying their signatures or expiration times, stack traces are exposed to callers, role-based access to documents has not been verified, and dependencies have not been checked for known vulnerabilities. You are the developer assigned to address each of these security gaps, one component at a time, without disrupting the service's existing functionality.
Lab Info
Table of Contents
-
Challenge
Step 1: Meet the document portal and its OWASP exposure
Welcome to the document portal
This is the internal portal your team uses to upload, tag, and download company documents. It works (people can log in, attach files, and share them), but a recent security review scored it against the OWASP Top 10 and found trouble in nearly every category. Over the next four steps you will fix each finding directly in the backend code, one component at a time, without breaking the workflows that already function.
This step is a walkthrough only: no code changes yet. Read each finding below so you know exactly which vulnerability class you're about to fix and which step fixes it. ### Finding 1: Untrusted input reaches the filesystem and the browser
The document download endpoint takes a filename straight from the request and hands it to the filesystem. A request for
../../etc/passwdis honored exactly as written: classic path traversal (OWASP A01/A03). Separately, the comment field on a shared document is rendered back to other users byte-for-byte, so a comment containing<script>runs in every viewer's browser (injection / XSS). Step 2 builds the filename guard and output encoder that close both gaps. ### Finding 2: Credentials are weakly protected in transit and at restLogin currently accepts requests over plain HTTP and compares the submitted password to the stored one with a simple string equals: no hashing, no salt, no TLS requirement (OWASP A02 and A07). Step 3 adds a channel check that refuses insecure connections and replaces the comparison with PBKDF2-based salted hashing. ### Finding 3: Session and token handling can be guessed or forged
Session IDs are generated with a plain pseudo-random generator, so consecutive sessions are predictable. JWTs presented on later requests are decoded and trusted without ever checking their signature or expiry, so a forged or stale token is accepted (OWASP A02/A07). Step 4 replaces the ID generator with a SecureRandom-backed one and builds a validator that checks both signature and expiry. ### Finding 4: Failures, roles, and dependencies were never reviewed
When something goes wrong, the raw exception (message, stack trace, and all) is sent straight back to the caller, leaking internals (OWASP A05). Nobody has verified that a Viewer really can't reach documents marked Internal, and nobody has checked whether the libraries in use match versions with known OWASP-relevant CVEs (A01, A06). Step 5 wires in a safe-response fallback, a redacting logger, a role-based access check, and a dependency auditor to close out the review.
-
Challenge
Step 2: Stop injection and path traversal at the edges
Stopping path traversal before it reaches the filesystem
The download endpoint currently passes whatever filename a caller sends straight into a file lookup. A caller can walk out of the documents directory entirely by sending
### Neutralizing injected markup on the way out..segments, or embed a path separator to reach an arbitrary path. The fix is an allow-list: decide up front what a legitimate filename looks like, and refuse everything else.A filename guard stops bad input from reaching the filesystem, but the comment field has a different exposure: it's rendered straight into another user's browser. If a comment contains
<script>...</script>, that script executes for every viewer who opens the document. This is a stored cross-site scripting flaw. The fix isn't to reject the comment; it's to encode it before it's written into HTML, so angle brackets and quotes display as text instead of being interpreted as markup. Order matters: escape ampersands first, so you don't double-encode the entities you're about to insert. -
Challenge
Step 3: Protect passwords and the channel that carries them
Requiring a secure channel before touching credentials
Even a perfectly hashed password is exposed if it's sent over plain HTTP, where it travels in cleartext across the network. Before the portal does anything with login data, it needs to refuse the request outright unless the connection reports itself as secure. This is a policy check, not a cryptographic one. It's the gate that has to pass before hashing and verification even get a chance to run. ### Hashing passwords the way they're meant to be stored
With the channel secured, the next problem is what happens to the password once it arrives: the portal currently compares it to a plaintext value with a simple equals check, so a leaked database hands over every password immediately. PBKDF2 exists specifically for this case. It derives a slow, salted key from the password so that even a stolen hash is expensive to reverse. A fresh random salt per password means two identical passwords never produce the same stored value. ### Verifying a login attempt against the stored hash
Hashing on the way in is only half the job; login needs a matching check on the way back out. Verification has to extract the same salt that was used originally, rerun the identical PBKDF2 derivation on the submitted password, and then compare the two hashes without leaking timing information through an ordinary equals call.
-
Challenge
Step 4: Issue and verify session and token credentials
Making session IDs impossible to guess
A session ID is effectively a password that the portal hands out itself, so it needs the same unpredictability. A generator built on an ordinary pseudo-random source, such as
java.util.Random, is seeded and reproducible—an attacker who can observe a few IDs can often predict the next one.SecureRandomdraws from an unpredictable entropy source built for exactly this purpose, and enough bytes from it make guessing infeasible. ### Trusting only tokens that are actually signedThe portal accepts JWTs on later requests but never checks whether the signature was produced with its own secret—which means anyone can hand-craft a payload with
role: ADMINand have it accepted. A JWT is three base64 segments joined by dots: header, payload, and signature. Validating it means recomputing the signature over the header and payload with the shared secret and comparing that to what the token actually carries. ### Rejecting tokens that have expired or been alteredA correctly signed token can still be a problem if it's stale: JWTs carry an
expclaim for exactly this reason, and a validator that ignores it will happily accept a token issued weeks ago. The full validation path checks the signature first—an unsigned token's claims mean nothing—and only then reads the expiry claim from the payload to decide whether the token is still current. -
Challenge
Step 5: Fail safely and audit what remains
Failing without leaking what went wrong
Right now, when a request fails inside the portal, the exception's message and stack trace go straight back to the caller: file paths, class names, sometimes even SQL fragments. Callers only ever need to know that something failed, not why, so every exception should collapse into the same generic response before it leaves the service. ### Keeping the diagnostic detail, minus the secrets
Hiding details from callers doesn't mean losing the information needed to troubleshoot failures. Operators still need diagnostic information, but that information belongs in logs, not in responses. Logs may be accessible to more people than the code that generates them, and copying raw error messages can expose sensitive information such as passwords or tokens. A logging method that redacts recognizable sensitive fields before formatting the entry preserves useful diagnostic context without exposing those values. ### Checking that roles actually restrict what they claim to
The portal has role names (Admin, Editor, Viewer) and document classifications (Public, Internal, Confidential), but nobody has verified that a Viewer is actually blocked from an Internal document at the code level. The fix is a single access check that defines, explicitly, which classifications each role may reach, and denies everything else by default, including roles it doesn't recognize. ### Flagging dependencies the review already knows about
The security review didn't just look at your code. It also checked the versions of the libraries the portal depends on against known vulnerability advisories, and found a few matches. Rather than repeat that manual check by hand every time, the last piece automates it: pass in the dependency versions your project uses and compare them against the review's findings and report exactly which ones still need to be upgraded.
-
Challenge
Step 6: Watch the hardened portal refuse, sign and redact
Every task is done and graded. Now watch the pieces work together. You will run the hardened components on hostile input, check the credentials they produce, and compare what callers see with what the log keeps.
Run hostile input through the components
Open the Terminal tab and run:
bash runApp.sh requestsExpected result: The terminal displays filename validation results that accept a safe filename and reject unsafe filenames, an HTML-encoded comment that prevents script execution, and login results that reject HTTP connections while allowing HTTPS connections.
Read the four
Downloadlines after the heading.report-2024.pdf -> acceptedis a normal document name.../../etc/passwd,reports/q3.pdfandnotes.txt;rmare allrejected. YourisSafeFilename()from Task 2.1 refuses traversal, separators and anything outside the allow-list.The comment
Nice doc <script>alert('hi')</script>comes back asNice doc <script>alert('hi')</script>. That is yourencodeForHtml()from Task 2.2. A browser shows that text instead of running it.The last two lines are your
requireSecureChannel()from Task 3.1.Login over HTTP -> refused (SecurityException)sits next toLogin over HTTPS -> allowed. ### Check the credentials it hands outIn the Terminal, run:
bash runApp.sh credentialsExpected result: The terminal displays two different stored password hashes, successful and unsuccessful password verification results, three unique session IDs, and JWT validation results.
The same password,
correct horse, produces two different stored values, soSame password, same stored value? false. Each stored value contains a randomly generated salt and a PBKDF2-derived hash, separated by a colon (:). This demonstrates the password hashing implemented in Task 3.2. Task 3.3 reads this format to verify passwords, soverify(correct horse) = trueandverify(wrong horse) = false.The three session IDs are each
43 characterslong and contain URL-safe characters. Each displayed session ID is different, demonstrating the random token generation implemented in Task 4.1 using 32 bytes generated bySecureRandom.The last three lines test your JWT validator using tokens signed by the program:
Valid token -> acceptedconfirms that a correctly signed, unexpired token is accepted.Expired token -> rejectedconfirms that a correctly signed token is rejected when itsexp(expiration time) has passed, demonstrating Task 4.3.Tampered token -> rejectedconfirms that a token with a modified payload and an invalid signature is rejected, demonstrating the signature verification implemented in Task 4.2. ### See what callers get and what the log keeps
In the Terminal, run:
bash runApp.sh failuresExpected result: The terminal displays a generic error message, a log entry with redacted sensitive information, role-based access results, and a list of vulnerable dependencies.
The program creates an exception containing a SQL query and a file path.
Caller sees: An unexpected error occurredconfirms that these sensitive details are not exposed to the caller, demonstrating thetoSafeResponse()method implemented in Task 5.1.The log entry preserves useful context while redacting sensitive information:
LOGIN_FAILED: user=alice, password=REDACTED, token=REDACTED, ip=10.0.0.1This demonstrates Task 5.2, where sensitive values such as
passwordandtokenare redacted whileuserandipremain unchanged.The role access results demonstrate Task 5.3.
ADMINcan access all three classifications (PUBLIC,INTERNAL, andCONFIDENTIAL).EDITORcan accessPUBLICandINTERNAL, whileVIEWERcan access onlyPUBLIC. The unknown roleGUESTcannot access any classification.The final line,
Vulnerable dependencies: [log4j-core, commons-fileupload], demonstrates Task 5.4. The program flagslog4j-core 2.14.1andcommons-fileupload 1.3.2because their versions match the known-vulnerable versions identified during the security review.jackson-databind 2.15.2is not flagged because its version does not match the review's known-vulnerable version.The command completes and returns you to the terminal prompt. Throughout this lab, you have implemented and tested security controls that validate input, encode output, hash passwords, verify tokens, redact sensitive information, enforce access restrictions, and identify vulnerable dependencies.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.