Introduction

While conducting a pentest on this application, I quickly discovered that the application was running in production with debug mode enabled. Triggering application errors exposed internal application details including stack traces, backend logic, and configuration-related information. At first, this looked like a medium-severity information disclosure issue. The real impact became clear later.

The Initial Discovery

It started with a simple mistake. While probing the /api/ endpoint, I sent a request without specifying a valid route after it. Instead of returning a generic error response, the application exposed a debug page.

None
Figure 1: Requested Invalid Endpoint
None
Figure 2: Debug=True Message

The response revealed the application's internal URL routing and confirmed that the backend framework in use was Django.

The Fuzz-athon

After discovering that the application was running with debug mode enabled, I began intentionally triggering errors across different parts of the application. The responses exposed SQL queries, database-related details, and portions of backend application logic.

None

Then came the most critical disclosure.

One of these stack traces exposed the application's JWT signing key through the local variables section of the exception traceback.

None
Figure 3: JWT Signing Key Disclosure — 1

The key was disclosed during JWT processing after a malformed token triggered an exception in the authentication flow.

None
Figure 4: JWT Signing Key Disclosure — 2

Using the disclosed key, I generated forged JWTs for test accounts and successfully authenticated as arbitrary users.

At this point, the issue was no longer just information disclosure. Any user account could now be impersonated.

About the Application

The application also contained additional low-severity weaknesses which significantly increased the impact of the primary issue.

1. JWT has no expiration date

None
Figure 5: No expiration set

The exp claim wasn't present in the token, which means that as long as a user has a token, it can be used forever to authenticate to the application.

2. Concurrent Sessions were allowed

A user could stay logged in from multiple devices at the same time.

The Chain

Individually, these findings were low severity. The real risk became clear when these seemingly harmless issues were chained together with the secret key disclosure.

Here is how:

  1. Imagine if the secret key had not been exposed. No forged tokens, and no account takeovers.
  2. Imagine if the token had short expiration periods. Damage could still be done, but persistent access would be harder to maintain.
  3. Imagine if application wasn't launched in debug mode. Well then, this blog won't exist.
None
[Figure 7: The Full Attack Chain]

Compromised users may not suspect unauthorized access because concurrent sessions were allowed. The application also did not notify users about new logins.

Recommendations

  1. Never launch an application in debug mode in production. Debug error pages can leak sensitive information to attackers.
  2. Sensitive information such as secret keys should always be stored securely outside application's source code.
  3. JWTs should have short expiration periods.
  4. Applications should provide visibility into active sessions and notify users about new logins or suspicious authentication activity.

Key Insight:

Django attempts to filter sensitive settings like secret keys, tokens, passwords, API keys from debug pages. But local variables inside stack frames are not automatically sanitized. So, even if developers think Django protects secrets in debug pages, sensitive runtime variables can still leak through exception traces.

Conclusion

The most important lesson here is that attackers rarely rely on a single vulnerability. Real-world compromises often happen when multiple low or medium-severity issues quietly connect together.