DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • The DevSecOps Paradox: Why Security Automation Is Both Solving and Creating Pipeline Vulnerabilities
  • DevSecConflict: How Google Project Zero and FFmpeg Went Viral For All the Wrong Reasons
  • Evaluating AI Vulnerability Detection: How Reliable Are LLMs for Secure Coding?
  • Security Concerns in Open GPTs: Emerging Threats, Vulnerabilities, and Mitigation Strategies

Trending

  • How AI Is Rewriting the Rules of Software Security: Machine-Speed Delivery, Shifting Risk, and New Control Points
  • Lease Coordination Under Serializable Isolation in CockroachDB
  • Why Your RAG Pipeline Will Fail Without an MCP Server
  • Comparing Top Gen AI Frameworks for Java in 2026
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. Building an OWASP 2025 Security Scanner in 48 Hours

Building an OWASP 2025 Security Scanner in 48 Hours

A weekend project turned into discovering critical security flaws in production code, including authentication bypasses hiding in plain sight.

By 
Dinesh Elumalai user avatar
Dinesh Elumalai
DZone Core CORE ·
Dec. 01, 25 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
4.7K Views

Join the DZone community and get the full member experience.

Join For Free

OWASP dropped its 2025 Top 10 on November 6th with a brand-new category nobody saw coming: "Mishandling of Exceptional Conditions" (A10). I spent a weekend building a scanner to detect these issues and immediately found authentication bypasses in three different production codebases. The most common pattern? return True in exception handlers, effectively granting access whenever the auth service hiccups. This article walks through building the scanner, what I found, and why this matters way more than you think.

Friday Night: OWASP Releases Something Interesting

I was scrolling through Twitter when I saw the OWASP announcement. They'd just released the 2025 Top 10 list at the Global AppSec Conference. Most people were talking about Supply Chain Security moving up to #3, but something else caught my eye.

There was a completely new category at #10: Mishandling of Exceptional Conditions.

Now, I've been reviewing code for long enough to know that exception handling is where bugs hide. But a whole OWASP category dedicated to it? That's new. I downloaded the spec, and one sentence jumped out:

"Applications that return truthy values or grant access when exceptions occur create critical security vulnerabilities that are nearly impossible to detect through traditional testing."

I knew exactly what they were talking about. Let me show you the pattern that's probably in your codebase right now:

Python
 
def is_admin(user_id):
    try:
        user = database.get_user(user_id)
        return user.role == 'admin'
    except:
        return True  # "Just for testing, I'll fix this later"


See that return True? That's a fail-open vulnerability. When the database connection fails (and in microservices architectures, connections fail constantly), this function grants admin access to anyone.

I decided to spend the weekend building a scanner to find these patterns. By Sunday evening, I had a working tool and some genuinely surprising results.

The 48-Hour Build Timeline

Friday 11 PM: Research Phase

Downloaded the OWASP 2025 spec. Read through the CWE mappings for A10. Four critical patterns stood out: CWE-636 (fail-open), CWE-209 (info disclosure), CWE-252 (unchecked returns), and CWE-755 (improper exception handling).

Saturday 9 AM: Architecture Design

Decided on Python because the ast module gives you proper Abstract Syntax Tree parsing. This eliminates 90% of false positives compared to regex-based scanning. Drew up a simple pipeline: parse → detect → analyze → report.

Saturday 2 PM: First Working Detector

Got the fail-open detector working. Tested it on some old projects and immediately found three instances in code I'd written myself. That was humbling.

Saturday 8 PM: Multi-Language Support

Added regex patterns for JavaScript, Java, and Go. Not as accurate as AST parsing, but catches the obvious cases. Started thinking about reporting formats.

Sunday 10 AM: Beautiful Reports

Built an HTML report generator with embedded CSS. Nobody reads plain text security reports, but they'll look at a color-coded dashboard.

Sunday 6 PM: CI/CD Integration

Added SARIF output for GitHub Security tab integration. Wrote a GitHub Actions workflow. Now it can actually prevent vulnerable code from being merged.

The Technical Deep Dive: How AST Parsing Catches What Regex Misses

Here's why using Python's Abstract Syntax Tree is crucial. Consider this code:

Python
 
# Regex would flag this as vulnerable
def validate_input(data):
    """
    Old implementation used to return True on exception.
    Don't do that! It's a CWE-636 vulnerability.
    """
    try:
        schema.validate(data)
        return True
    except ValidationError:
        return False  # Correctly failing closed


A regex search for return True inside a try/except block would flag this as vulnerable because of the comment. AST parsing understands that the comment isn't executable code.

Here's the actual implementation:

Python
 
import ast

def detect_fail_open(file_path, content):
    tree = ast.parse(content)
    
    for node in ast.walk(tree):
        if isinstance(node, ast.Try):
            # Found a try/except block
            for handler in node.handlers:
                # Check each except handler
                for stmt in handler.body:
                    if isinstance(stmt, ast.Return):
                        # Found a return statement in an except block
                        if isinstance(stmt.value, ast.Constant):
                            if stmt.value.value is True:
                                # CRITICAL: Returns True on exception
                                return create_finding(
                                    severity="CRITICAL",
                                    cwe="CWE-636",
                                    line=stmt.lineno,
                                    message="Fail-open vulnerability detected"
                                )


This code understands the structure of your program, not just text patterns. It walks the syntax tree, finds Try nodes, examines exception handlers, looks for Return statements, and checks if they're returning True.

What I Found: Real Vulnerabilities in Real Code

I tested the scanner on three different codebases where I had permission to scan. Here's what turned up:


Finding #1: The Authentication Bypass That Lived for 18 Months

This one was in a payment processing service. Here's the actual code (with names changed):

Python
 
def verify_transaction_signature(transaction):
    """Verify the cryptographic signature on a transaction"""
    try:
        public_key = get_merchant_public_key(transaction.merchant_id)
        signature = base64.b64decode(transaction.signature)
        
        # Verify signature
        public_key.verify(signature, transaction.data)
        return True
    except Exception as e:
        # TODO: Log this properly
        return True  # Let it through for now


This code had been in production for 18 months. When the key service was unreachable (network partition, service restart, etc.), it approved every transaction. The comment says "for now," but it shipped, and nobody remembered to fix it.

Impact Analysis: During a 15-minute outage of the key service in August, this vulnerability would have allowed unauthorized transactions. They got lucky — no one tried to exploit it. But "we got lucky" is not a security strategy.

Finding #2: Information Disclosure in Error Messages

This pattern showed up in 12 different files across one codebase:

Python
 
@app.route('/api/user/<user_id>')
def get_user_details(user_id):
    try:
        user = User.query.get(user_id)
        return jsonify(user.to_dict())
    except Exception as e:
        return jsonify({
            'error': str(e),
            'traceback': traceback.format_exc()
        }), 500


Every time this endpoint threw an exception, it sent a complete stack trace to the client. This included:

  • Database connection strings (with redacted passwords, but still)
  • Internal server paths
  • Library versions
  • Query structures

An attacker could use this to map the entire internal architecture.

Finding #3: The Audit Log That Silently Failed

This one is subtle but nasty:

Python
 
def process_admin_action(user, action, resource):
    # Perform the action
    result = action.execute(resource)
    
    # Log it
    try:
        audit_log.write({
            'user': user.id,
            'action': action.name,
            'resource': resource.id,
            'timestamp': datetime.now()
        })
    except:
        pass  # Don't let logging failures break admin actions
    
    return result


The logic seems reasonable: don't let logging failures prevent admin actions from completing. But here's what actually happened:

The audit database ran out of disk space. For six hours, every admin action was completed successfully, but nothing was logged. By the time someone noticed, there was a gap in the audit trail during a period when sensitive data was accessed.

The correct fix? Alert when audit logging fails:

Python
 
try:
    audit_log.write(event)
except Exception as e:
    # This is CRITICAL - audit failures must be visible
    logger.critical(f"AUDIT FAILURE: {e}")
    alert_security_team(e)
    # Still return success, but make noise about it


The Pattern Everyone Misses: Database Timeouts

Here's something I noticed across all three codebases: developers test the happy path and the obvious failure cases, but nobody tests what happens when the database times out.


Your tests pass. Your code review passes. Everything looks good. Then, in production, a network blip happens during authentication, the exception handler runs, and suddenly you're granting access.

Why This Is Hard to Catch (And Why We Need Automated Tools)

Let me show you why traditional security testing misses fail-open vulnerabilities:


The problem is that fail-open vulnerabilities only manifest under specific failure conditions. Your auth service works fine in staging. It works fine in production 99.9% of the time. But that 0.1% when it doesn't? That's when the vulnerability activates.

Building the Detector: The Technical Approach

The core challenge was identifying return True statements that actually matter. Not every return True in an exception handler is dangerous. For example:

Python
 
def is_valid_email_format(email):
    """Check if email has valid format (not authentication!)"""
    try:
        validate_email_format(email)
        return True
    except FormatError:
        return False  # This is fine - just validation


This is perfectly safe because it's just format validation, not security. The scanner needs to understand context.

I used several heuristics:

  1. Function name analysis: Functions with names like check_permission, verify_*, is_admin, authenticate are flagged as high-priority
  2. Return value context: return True is more suspicious than return False in security contexts
  3. Exception type: Catching Exception or bare except: is more dangerous than catching specific exceptions
  4. Surrounding code: Database calls or API calls in the try block increase the risk

Here's the prioritization logic:


The Results Dashboard: Making Security Visible

Nobody reads text-only security reports. I learned this the hard way at my last job when I sent a 50-page PDF of findings and got back "looks good, thanks!"

So I built a visual dashboard. Here's what it shows:

Plain Text
 
╔══════════════════════════════════════════════════════════════╗
║                    SECURITY SCAN REPORT                      ║
║                   OWASP Top 10 2025 Scanner                  ║
╚══════════════════════════════════════════════════════════════╝

SUMMARY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Files Scanned:          312
  Scan Duration:          2.3s
  Total Issues:           23
  
 CRITICAL:  8    (Immediate action required)
 HIGH:      12   (Fix before next release)
 MEDIUM:    3    (Address in backlog)

ISSUES BY CATEGORY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  A02: Security Misconfiguration         █████████ 12
  A10: Exception Handling                 ████████  8
  A04: Cryptographic Failures             ██        3

TOP 5 CRITICAL ISSUES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Fail-Open Auth Check      src/auth.py:45      CWE-636
2. Hardcoded API Key          config.py:12        CWE-798
3. Fail-Open Admin Check      admin/views.py:89   CWE-636
4. Database Password in Code  settings.py:34      CWE-798
5. Info Disclosure in Error   api/handlers.py:67  CWE-209


This gets attention. People actually read it and ask questions.

CI/CD Integration: Preventing Future Vulnerabilities

The scanner is only useful if it's in your pipeline. I built a GitHub Actions workflow that runs on every pull request:

YAML
 
name: Security Scan

on: [pull_request]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Run OWASP 2025 Scanner
        run: |
          pip install owasp-scanner-2025
          owasp-scan . \
            --categories A02,A10 \
            --fail-on critical \
            --formats sarif,json
      
      - name: Upload results to GitHub Security
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: security-reports/*.sarif
      
      - name: Comment on PR
        if: failure()
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: 'Security scan found critical issues. Check the Security tab for details.'
            })


The --fail-on critical flag means the PR can't be merged if critical issues are found. This prevents the "I'll fix it later" problem.

Lessons Learned: What Surprised Me

After scanning dozens of repositories, some patterns emerged that I didn't expect:

1. Fail-Open Is More Common Than You Think

I expected to find maybe one or two instances. Instead, I found fail-open patterns in roughly 15% of codebases. They cluster around certain types of operations:

  • Authentication and authorization (most dangerous)
  • Rate limiting ("let it through if Redis is down")
  • Feature flags ("default to enabled on error")
  • Payment verification ("process it anyway")

2. The Comments Tell a Story

Almost every fail-open vulnerability had a comment nearby:

  • "TODO: Fix this properly"
  • "Temporary workaround"
  • "Just for testing"
  • "Fix before production" (it was in production)

These were all well-intentioned temporary fixes that became permanent.

3. Nobody Tests the Failure Cases

I looked at the test suites for several projects. They had excellent coverage of the happy path. But almost zero tests for "what happens when the database is unreachable."

The reason? It's hard to test. You need to mock network failures, database timeouts, and service unavailability. Most test suites aren't set up for that level of chaos engineering.

4. Senior Developers Write This Code Too

This isn't a junior developer problem. I found fail-open patterns in code written by senior engineers, architects, and even security-focused developers. The issue isn't skill level - it's visibility. Exception handlers are usually written last, often during debugging, and don't get the same scrutiny as main logic paths.

The Fix: How to Write Exception Handlers Correctly

Here's the pattern I recommend for security-critical exception handling:

Python
 
###WRONG: Fail-Open####

def check_permission(user, resource):
    try:
        return auth_service.verify(user, resource)
    except Exception:
        return True


Python
 
####CORRECT: Fail-Closed####

def check_permission(user, resource):
    try:
        return auth_service.verify(user, resource)
    except Exception as e:
        logger.error(f"Auth failed: {e}", extra={
            'user_id': user.id,
            'resource': resource.id
        })
        metrics.increment('auth.errors')
        return False  # Deny by default


The key principles:

  1. Fail closed: Default to the secure option (deny access, block request, etc.)
  2. Log it: Make failures visible so you know when systems are failing
  3. Monitor it: Increment metrics so you can alert on error spikes
  4. Be specific: Catch specific exceptions when possible, not broad Exception

What's Next: The Roadmap

The scanner works, but there's room for improvement:

Planned Features

  • JavaScript/TypeScript AST parsing – Currently using regex for JS, want full AST analysis
  • Machine learning for false positive reduction – Train on labeled examples to improve accuracy
  • Runtime monitoring integration – Hook into APM tools to catch exceptions that only occur in production
  • Fix suggestions – Not just "this is wrong" but "here's the correct code"
  • Severity customization – Let teams define their own risk thresholds

Try It Yourself

The scanner is open source and available now. Here's how to use it:

Shell
 
# Install
pip install owasp-scanner-2025

# Scan your codebase
owasp-scan /path/to/your/project

# Focus on critical issues only
owasp-scan . --severity critical,high

# Generate a report
owasp-scan . --formats html,json --output ./reports

# Use in CI/CD (fail build on critical issues)
owasp-scan . --fail-on critical


GitHub repository: github.com/dinesh-k-elumalai/owasp-scanner-2025

Final Thoughts: Exception Handling Is Security

Here's what this weekend project taught me: we've been thinking about exception handling wrong.

We treat it as error recovery - a way to gracefully handle failures and keep the application running. And that's true. But in security-critical code paths, exception handlers are part of your security boundary.

When you write return True in an exception handler, you're not just handling an error. You're making a security decision: what happens when authentication fails, when the crypto library throws an exception, when the rate limiter can't reach Redis.

OWASP's decision to add "Mishandling of Exceptional Conditions" as a standalone category recognizes this reality. In modern distributed systems with dozens of microservices, network calls to external APIs, and cloud service dependencies, failures are the norm, not the exception.

Your exception handlers will run in production. Probably more often than you think. The question is: when they do, will they make the secure choice?

Key Takeaways

  • Fail-open patterns (CWE-636) are more common than anyone realizes
  • Traditional testing misses these vulnerabilities because they only manifest during failures
  • AST-based static analysis can catch these issues before they reach production
  • Exception handlers in security-critical code need the same scrutiny as main logic paths
  • The secure default is always "deny" - fail closed, not open

If you take away one thing from this article, make it this: next time you write an exception handler in authentication, authorization, or any security-critical code, ask yourself: "If this exception actually fires in production, am I granting access or denying it?"

Because somewhere in your codebase, there's probably a return True that shouldn't be there.

Common Weakness Enumeration Vulnerability security

Opinions expressed by DZone contributors are their own.

Related

  • The DevSecOps Paradox: Why Security Automation Is Both Solving and Creating Pipeline Vulnerabilities
  • DevSecConflict: How Google Project Zero and FFmpeg Went Viral For All the Wrong Reasons
  • Evaluating AI Vulnerability Detection: How Reliable Are LLMs for Secure Coding?
  • Security Concerns in Open GPTs: Emerging Threats, Vulnerabilities, and Mitigation Strategies

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook