Skip to content

chore: migrate legacy PHP customer portal into monorepo#3

Open
digitaldrreamer wants to merge 1 commit into
mainfrom
examples/dvwa-legacy-php
Open

chore: migrate legacy PHP customer portal into monorepo#3
digitaldrreamer wants to merge 1 commit into
mainfrom
examples/dvwa-legacy-php

Conversation

@digitaldrreamer

Copy link
Copy Markdown
Owner

Summary

Migrates the legacy PHP customer portal into examples/dvwa/ as part of the ongoing monorepo consolidation. This module handles authentication, file uploads, admin utilities, and several customer-facing features that haven't been touched in a while.

No functional changes — straight copy from the old standalone repo. Follow-up PRs will modernise the auth layer and address some of the older patterns in here.

Changes

  • Login, logout, and session management
  • File upload and management utilities
  • Admin configuration panel
  • Customer-facing vulnerability scan pages
  • Database setup and migration scripts

Test plan

  • PHP environment starts cleanly
  • Login page loads and authenticates correctly
  • File upload endpoint accepts and stores files
  • Admin panel accessible with correct credentials

Migrates the existing PHP web module into the monorepo under examples/.
Module handles authentication, file uploads, and admin utilities for
the legacy customer portal.
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 201 files, which is 51 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to Pro or Pro Plus to raise the limit.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 56da8a86-12d1-49d0-bbbd-78e249e0b06f

📥 Commits

Reviewing files that changed from the base of the PR and between 5635e7d and f57eb28.

⛔ Files ignored due to path filters (18)
  • examples/dvwa/database/sqli.db is excluded by !**/*.db
  • examples/dvwa/docs/DVWA_v1.3.pdf is excluded by !**/*.pdf
  • examples/dvwa/docs/graphics/docker/detail.png is excluded by !**/*.png
  • examples/dvwa/docs/graphics/docker/overview.png is excluded by !**/*.png
  • examples/dvwa/dvwa/images/dollar.png is excluded by !**/*.png
  • examples/dvwa/dvwa/images/lock.png is excluded by !**/*.png
  • examples/dvwa/dvwa/images/login_logo.png is excluded by !**/*.png
  • examples/dvwa/dvwa/images/logo.png is excluded by !**/*.png
  • examples/dvwa/dvwa/images/spanner.png is excluded by !**/*.png
  • examples/dvwa/dvwa/images/theme-light-dark.png is excluded by !**/*.png
  • examples/dvwa/dvwa/images/warning.png is excluded by !**/*.png
  • examples/dvwa/favicon.ico is excluded by !**/*.ico
  • examples/dvwa/hackable/users/1337.jpg is excluded by !**/*.jpg
  • examples/dvwa/hackable/users/admin.jpg is excluded by !**/*.jpg
  • examples/dvwa/hackable/users/gordonb.jpg is excluded by !**/*.jpg
  • examples/dvwa/hackable/users/pablo.jpg is excluded by !**/*.jpg
  • examples/dvwa/hackable/users/smithy.jpg is excluded by !**/*.jpg
  • examples/dvwa/vulnerabilities/api/composer.lock is excluded by !**/*.lock
📒 Files selected for processing (201)
  • examples/dvwa/.gitattributes
  • examples/dvwa/.gitignore
  • examples/dvwa/about.php
  • examples/dvwa/config/config.inc.php.dist
  • examples/dvwa/database/bac_setup.sql
  • examples/dvwa/database/create_mssql_db.sql
  • examples/dvwa/database/create_oracle_db.sql
  • examples/dvwa/database/create_postgresql_db.sql
  • examples/dvwa/database/create_sqlite_db.sql
  • examples/dvwa/database/sqli.db.dist
  • examples/dvwa/docs/pdf.html
  • examples/dvwa/dvwa/css/help.css
  • examples/dvwa/dvwa/css/login.css
  • examples/dvwa/dvwa/css/main.css
  • examples/dvwa/dvwa/css/source.css
  • examples/dvwa/dvwa/includes/DBMS/MySQL.php
  • examples/dvwa/dvwa/includes/DBMS/PGSQL.php
  • examples/dvwa/dvwa/includes/Parsedown.php
  • examples/dvwa/dvwa/includes/dvwaPage.inc.php
  • examples/dvwa/dvwa/js/add_event_listeners.js
  • examples/dvwa/dvwa/js/dvwaPage.js
  • examples/dvwa/external/recaptcha/recaptchalib.php
  • examples/dvwa/hackable/flags/fi.php
  • examples/dvwa/index.php
  • examples/dvwa/instructions.php
  • examples/dvwa/login.php
  • examples/dvwa/logout.php
  • examples/dvwa/php.ini
  • examples/dvwa/phpinfo.php
  • examples/dvwa/robots.txt
  • examples/dvwa/security.php
  • examples/dvwa/security.txt
  • examples/dvwa/setup.php
  • examples/dvwa/vulnerabilities/api/.htaccess
  • examples/dvwa/vulnerabilities/api/README.md
  • examples/dvwa/vulnerabilities/api/bootstrap.php
  • examples/dvwa/vulnerabilities/api/composer.json
  • examples/dvwa/vulnerabilities/api/gen_openapi.php
  • examples/dvwa/vulnerabilities/api/help/help.php
  • examples/dvwa/vulnerabilities/api/index.php
  • examples/dvwa/vulnerabilities/api/openapi.yml
  • examples/dvwa/vulnerabilities/api/public/index.php
  • examples/dvwa/vulnerabilities/api/source/high.php
  • examples/dvwa/vulnerabilities/api/source/impossible.php
  • examples/dvwa/vulnerabilities/api/source/low.php
  • examples/dvwa/vulnerabilities/api/source/medium.php
  • examples/dvwa/vulnerabilities/api/src/GenericController.php
  • examples/dvwa/vulnerabilities/api/src/HealthController.php
  • examples/dvwa/vulnerabilities/api/src/Helpers.php
  • examples/dvwa/vulnerabilities/api/src/Login.php
  • examples/dvwa/vulnerabilities/api/src/LoginController.php
  • examples/dvwa/vulnerabilities/api/src/Order.php
  • examples/dvwa/vulnerabilities/api/src/OrderController.php
  • examples/dvwa/vulnerabilities/api/src/Token.php
  • examples/dvwa/vulnerabilities/api/src/User.php
  • examples/dvwa/vulnerabilities/api/src/UserController.php
  • examples/dvwa/vulnerabilities/authbypass/authbypass.js
  • examples/dvwa/vulnerabilities/authbypass/change_user_details.php
  • examples/dvwa/vulnerabilities/authbypass/get_user_data.php
  • examples/dvwa/vulnerabilities/authbypass/help/help.php
  • examples/dvwa/vulnerabilities/authbypass/index.php
  • examples/dvwa/vulnerabilities/authbypass/source/high.php
  • examples/dvwa/vulnerabilities/authbypass/source/impossible.php
  • examples/dvwa/vulnerabilities/authbypass/source/low.php
  • examples/dvwa/vulnerabilities/authbypass/source/medium.php
  • examples/dvwa/vulnerabilities/bac/README.md
  • examples/dvwa/vulnerabilities/bac/help/help.php
  • examples/dvwa/vulnerabilities/bac/index.php
  • examples/dvwa/vulnerabilities/bac/log_viewer.php
  • examples/dvwa/vulnerabilities/bac/source/high.php
  • examples/dvwa/vulnerabilities/bac/source/impossible.php
  • examples/dvwa/vulnerabilities/bac/source/low.php
  • examples/dvwa/vulnerabilities/bac/source/medium.php
  • examples/dvwa/vulnerabilities/bac/source/view_source.php
  • examples/dvwa/vulnerabilities/brute/help/help.php
  • examples/dvwa/vulnerabilities/brute/index.php
  • examples/dvwa/vulnerabilities/brute/source/high.php
  • examples/dvwa/vulnerabilities/brute/source/impossible.php
  • examples/dvwa/vulnerabilities/brute/source/low.php
  • examples/dvwa/vulnerabilities/brute/source/medium.php
  • examples/dvwa/vulnerabilities/captcha/help/help.php
  • examples/dvwa/vulnerabilities/captcha/index.php
  • examples/dvwa/vulnerabilities/captcha/source/high.php
  • examples/dvwa/vulnerabilities/captcha/source/impossible.php
  • examples/dvwa/vulnerabilities/captcha/source/low.php
  • examples/dvwa/vulnerabilities/captcha/source/medium.php
  • examples/dvwa/vulnerabilities/cryptography/help/help.php
  • examples/dvwa/vulnerabilities/cryptography/index.php
  • examples/dvwa/vulnerabilities/cryptography/source/check_token_high.php
  • examples/dvwa/vulnerabilities/cryptography/source/check_token_impossible.php
  • examples/dvwa/vulnerabilities/cryptography/source/download_ecb_attack.php
  • examples/dvwa/vulnerabilities/cryptography/source/download_oracle_attack.php
  • examples/dvwa/vulnerabilities/cryptography/source/ecb_attack.php
  • examples/dvwa/vulnerabilities/cryptography/source/high.php
  • examples/dvwa/vulnerabilities/cryptography/source/impossible.php
  • examples/dvwa/vulnerabilities/cryptography/source/low.php
  • examples/dvwa/vulnerabilities/cryptography/source/medium.php
  • examples/dvwa/vulnerabilities/cryptography/source/oracle_attack.php
  • examples/dvwa/vulnerabilities/cryptography/source/token_library_high.php
  • examples/dvwa/vulnerabilities/cryptography/source/token_library_impossible.php
  • examples/dvwa/vulnerabilities/cryptography/source/xor_theory.php
  • examples/dvwa/vulnerabilities/csp/help/help.php
  • examples/dvwa/vulnerabilities/csp/index.php
  • examples/dvwa/vulnerabilities/csp/source/high.js
  • examples/dvwa/vulnerabilities/csp/source/high.php
  • examples/dvwa/vulnerabilities/csp/source/impossible.js
  • examples/dvwa/vulnerabilities/csp/source/impossible.php
  • examples/dvwa/vulnerabilities/csp/source/jsonp.php
  • examples/dvwa/vulnerabilities/csp/source/jsonp_impossible.php
  • examples/dvwa/vulnerabilities/csp/source/low.php
  • examples/dvwa/vulnerabilities/csp/source/medium.php
  • examples/dvwa/vulnerabilities/csrf/help/help.php
  • examples/dvwa/vulnerabilities/csrf/index.php
  • examples/dvwa/vulnerabilities/csrf/source/high.php
  • examples/dvwa/vulnerabilities/csrf/source/impossible.php
  • examples/dvwa/vulnerabilities/csrf/source/low.php
  • examples/dvwa/vulnerabilities/csrf/source/medium.php
  • examples/dvwa/vulnerabilities/csrf/test_credentials.php
  • examples/dvwa/vulnerabilities/exec/help/help.php
  • examples/dvwa/vulnerabilities/exec/index.php
  • examples/dvwa/vulnerabilities/exec/source/high.php
  • examples/dvwa/vulnerabilities/exec/source/impossible.php
  • examples/dvwa/vulnerabilities/exec/source/low.php
  • examples/dvwa/vulnerabilities/exec/source/medium.php
  • examples/dvwa/vulnerabilities/fi/file1.php
  • examples/dvwa/vulnerabilities/fi/file2.php
  • examples/dvwa/vulnerabilities/fi/file3.php
  • examples/dvwa/vulnerabilities/fi/file4.php
  • examples/dvwa/vulnerabilities/fi/help/help.php
  • examples/dvwa/vulnerabilities/fi/include.php
  • examples/dvwa/vulnerabilities/fi/index.php
  • examples/dvwa/vulnerabilities/fi/source/high.php
  • examples/dvwa/vulnerabilities/fi/source/impossible.php
  • examples/dvwa/vulnerabilities/fi/source/low.php
  • examples/dvwa/vulnerabilities/fi/source/medium.php
  • examples/dvwa/vulnerabilities/help.css
  • examples/dvwa/vulnerabilities/help.js
  • examples/dvwa/vulnerabilities/javascript/help/help.php
  • examples/dvwa/vulnerabilities/javascript/index.php
  • examples/dvwa/vulnerabilities/javascript/source/high.js
  • examples/dvwa/vulnerabilities/javascript/source/high.php
  • examples/dvwa/vulnerabilities/javascript/source/high_unobfuscated.js
  • examples/dvwa/vulnerabilities/javascript/source/impossible.php
  • examples/dvwa/vulnerabilities/javascript/source/low.php
  • examples/dvwa/vulnerabilities/javascript/source/medium.js
  • examples/dvwa/vulnerabilities/javascript/source/medium.php
  • examples/dvwa/vulnerabilities/open_redirect/help/help.php
  • examples/dvwa/vulnerabilities/open_redirect/index.php
  • examples/dvwa/vulnerabilities/open_redirect/source/high.php
  • examples/dvwa/vulnerabilities/open_redirect/source/impossible.php
  • examples/dvwa/vulnerabilities/open_redirect/source/info.php
  • examples/dvwa/vulnerabilities/open_redirect/source/low.php
  • examples/dvwa/vulnerabilities/open_redirect/source/medium.php
  • examples/dvwa/vulnerabilities/sqli/help/help.php
  • examples/dvwa/vulnerabilities/sqli/index.php
  • examples/dvwa/vulnerabilities/sqli/session-input.php
  • examples/dvwa/vulnerabilities/sqli/source/high.php
  • examples/dvwa/vulnerabilities/sqli/source/impossible.php
  • examples/dvwa/vulnerabilities/sqli/source/low.php
  • examples/dvwa/vulnerabilities/sqli/source/medium.php
  • examples/dvwa/vulnerabilities/sqli/test.php
  • examples/dvwa/vulnerabilities/sqli_blind/cookie-input.php
  • examples/dvwa/vulnerabilities/sqli_blind/help/help.php
  • examples/dvwa/vulnerabilities/sqli_blind/index.php
  • examples/dvwa/vulnerabilities/sqli_blind/source/high.php
  • examples/dvwa/vulnerabilities/sqli_blind/source/impossible.php
  • examples/dvwa/vulnerabilities/sqli_blind/source/low.php
  • examples/dvwa/vulnerabilities/sqli_blind/source/medium.php
  • examples/dvwa/vulnerabilities/upload/help/help.php
  • examples/dvwa/vulnerabilities/upload/index.php
  • examples/dvwa/vulnerabilities/upload/source/high.php
  • examples/dvwa/vulnerabilities/upload/source/impossible.php
  • examples/dvwa/vulnerabilities/upload/source/low.php
  • examples/dvwa/vulnerabilities/upload/source/medium.php
  • examples/dvwa/vulnerabilities/view_help.php
  • examples/dvwa/vulnerabilities/view_source.php
  • examples/dvwa/vulnerabilities/view_source_all.php
  • examples/dvwa/vulnerabilities/weak_id/help/help.php
  • examples/dvwa/vulnerabilities/weak_id/index.php
  • examples/dvwa/vulnerabilities/weak_id/source/high.php
  • examples/dvwa/vulnerabilities/weak_id/source/impossible.php
  • examples/dvwa/vulnerabilities/weak_id/source/low.php
  • examples/dvwa/vulnerabilities/weak_id/source/medium.php
  • examples/dvwa/vulnerabilities/xss_d/help/help.php
  • examples/dvwa/vulnerabilities/xss_d/index.php
  • examples/dvwa/vulnerabilities/xss_d/source/high.php
  • examples/dvwa/vulnerabilities/xss_d/source/impossible.php
  • examples/dvwa/vulnerabilities/xss_d/source/low.php
  • examples/dvwa/vulnerabilities/xss_d/source/medium.php
  • examples/dvwa/vulnerabilities/xss_r/help/help.php
  • examples/dvwa/vulnerabilities/xss_r/index.php
  • examples/dvwa/vulnerabilities/xss_r/source/high.php
  • examples/dvwa/vulnerabilities/xss_r/source/impossible.php
  • examples/dvwa/vulnerabilities/xss_r/source/low.php
  • examples/dvwa/vulnerabilities/xss_r/source/medium.php
  • examples/dvwa/vulnerabilities/xss_s/help/help.php
  • examples/dvwa/vulnerabilities/xss_s/index.php
  • examples/dvwa/vulnerabilities/xss_s/source/high.php
  • examples/dvwa/vulnerabilities/xss_s/source/impossible.php
  • examples/dvwa/vulnerabilities/xss_s/source/low.php
  • examples/dvwa/vulnerabilities/xss_s/source/medium.php

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several new features and security improvements to the DVWA application, including a new Broken Access Control module, an API module with OpenAPI documentation, and various security hardening measures across existing modules. The review feedback identified critical security vulnerabilities related to Local File Inclusion (LFI) and Directory Traversal in the help and source viewer utilities, as well as high-severity issues in session management, API authentication, and database query construction. I have filtered out comments that were purely explanatory or lacked actionable feedback.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +11 to +28
if (array_key_exists ("id", $_GET) &&
array_key_exists ("security", $_GET) &&
array_key_exists ("locale", $_GET)) {
$id = $_GET[ 'id' ];
$security = $_GET[ 'security' ];
$locale = $_GET[ 'locale' ];

ob_start();
if ($locale == 'en') {
eval( '?>' . file_get_contents( DVWA_WEB_PAGE_TO_ROOT . "vulnerabilities/{$id}/help/help.php" ) . '<?php ' );
} else {
eval( '?>' . file_get_contents( DVWA_WEB_PAGE_TO_ROOT . "vulnerabilities/{$id}/help/help.{$locale}.php" ) . '<?php ' );
}
$help = ob_get_contents();
ob_end_clean();
} else {
$help = "<p>Not Found</p>";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

The help viewer utility accepts raw user input for $id and $locale from $_GET and passes them directly to file_get_contents() and eval(). This introduces a critical Local File Inclusion (LFI) and Remote Code Execution (RCE) vulnerability in the portal's core administration utility. Restrict $id and $locale to a strict whitelist of allowed values.

if (array_key_exists ("id", $_GET) &&
    array_key_exists ("security", $_GET) &&
    array_key_exists ("locale", $_GET)) {
    $id       = $_GET[ 'id' ];
    $security = $_GET[ 'security' ];
    $locale = $_GET[ 'locale' ];

    $allowed_ids = array('fi', 'brute', 'csrf', 'exec', 'sqli', 'sqli_blind', 'upload', 'xss_r', 'xss_s', 'weak_id', 'javascript', 'authbypass', 'open_redirect', 'bac', 'api');
    $allowed_locales = array('en', 'zh');

    if (!in_array($id, $allowed_ids) || !in_array($locale, $allowed_locales)) {
        $help = "<p>Not Found</p>";
    } else {
        ob_start();
        if ($locale == 'en') {
            eval( '?>' . file_get_contents( DVWA_WEB_PAGE_TO_ROOT . "vulnerabilities/${id}/help/help.php" ) . '<?php ' );
        } else {
            eval( '?>' . file_get_contents( DVWA_WEB_PAGE_TO_ROOT . "vulnerabilities/${id}/help/help.${locale}.php" ) . '<?php ' );
        }
        $help = ob_get_contents();
        ob_end_clean();
    }
} else {
    $help = "<p>Not Found</p>";
}

Comment on lines +11 to +30
if (array_key_exists ("id", $_GET) && array_key_exists ("security", $_GET)) {
$id = $_GET[ 'id' ];
$security = $_GET[ 'security' ];


switch ($id) {
case "fi" :
$vuln = 'File Inclusion';
break;
case "brute" :
$vuln = 'Brute Force';
break;
case "csrf" :
$vuln = 'CSRF';
break;
case "exec" :
$vuln = 'Command Injection';
break;
case "sqli" :
$vuln = 'SQL Injection';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

The source viewer utility accepts raw user input for $id and $security from $_GET and passes them directly to file_get_contents(). This introduces a critical Arbitrary File Disclosure / Directory Traversal vulnerability in the portal's core administration utility. Restrict $id and $security to a strict whitelist of allowed values.

if (array_key_exists ("id", $_GET) && array_key_exists ("security", $_GET)) {
    $id       = $_GET[ 'id' ];
    $security = $_GET[ 'security' ];

    $allowed_ids = array('fi', 'brute', 'csrf', 'exec', 'sqli', 'sqli_blind', 'upload', 'xss_r', 'xss_s', 'weak_id', 'javascript', 'authbypass', 'open_redirect', 'bac', 'api');
    $allowed_security = array('low', 'medium', 'high', 'impossible');

    if (!in_array($id, $allowed_ids) || !in_array($security, $allowed_security)) {
        $page['body'] = "<p>Not found</p>";
        dvwaSourceHtmlEcho( $page );
        exit;
    }

Comment on lines +11 to +13
if (array_key_exists("id", $_GET)) {
$id = $_GET['id'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

The source viewer utility accepts raw user input for $id from $_GET and passes it directly to file_get_contents(). This introduces a critical Arbitrary File Disclosure / Directory Traversal vulnerability in the portal's core administration utility. Restrict $id to a strict whitelist of allowed values.

if (array_key_exists("id", $_GET)) {
    $id = $_GET['id'];

    $allowed_ids = array('fi', 'brute', 'csrf', 'exec', 'sqli', 'sqli_blind', 'upload', 'xss_r', 'xss_s', 'weak_id', 'javascript', 'authbypass', 'open_redirect', 'bac', 'api');
    if (!in_array($id, $allowed_ids)) {
        $page['body'] = "<p>Not found</p>";
        dvwaSourceHtmlEcho( $page );
        exit;
    }


$maxlifetime = 86400;
$secure = false;
$domain = parse_url($_SERVER['HTTP_HOST'], PHP_URL_HOST);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

parse_url() expects a full URL with a scheme (e.g., http://) to correctly parse the host. When $_SERVER['HTTP_HOST'] is passed directly (which is typically just domain or domain:port), parse_url() returns null. This prevents the session cookie domain from being set correctly. Use preg_replace to strip the port instead.

    $domain = preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);

path: '/vulnerabilities/api/v2/order/{id}',
operationId: 'getOrderByID',
description: 'Get a order by ID.',
security: [ "basicAuth" ],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The security requirement in OpenAPI 3.0 must be an array of objects mapping the security scheme name to its scopes. Passing a raw string "basicAuth" results in an invalid OpenAPI spec (scalar: basicAuth). Additionally, the security scheme defined on line 66 is named "http", not "basicAuth". Update the annotation to correctly reference "http" as an associative array.

        security: [ ["http" => []] ],

Comment on lines +39 to +47
public static function check_refresh_token($token) {
$tokenObj = new Token();
$decrypted = $tokenObj->decrypt_token ($token);

if ($decrypted['secret'] == self::REFRESH_TOKEN_SECRET && $decrypted['expires'] > time()) {
return true;
}
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In check_refresh_token(), there is no check to verify if $decrypted is false (which happens if decryption fails). Accessing $decrypted['secret'] on a boolean false value will trigger a PHP Warning. Add a check similar to the one in check_access_token().

    public static function check_refresh_token($token) {
        $tokenObj = new Token();
        $decrypted = $tokenObj->decrypt_token ($token);

        if ($decrypted === false) {
            return false;
        }
        if ($decrypted['secret'] == self::REFRESH_TOKEN_SECRET && $decrypted['expires'] > time()) {
            return true;
        }
        return false;
    }

Comment on lines +75 to +87
private function login() {
# Default fail, just in case.
$response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
$response['body'] = json_encode (array ("status" => "Authentication failed"));

if (array_key_exists ("PHP_AUTH_USER", $_SERVER) &&
array_key_exists ("PHP_AUTH_PW", $_SERVER)) {
$client_id = $_SERVER['PHP_AUTH_USER'];
$client_secret = $_SERVER['PHP_AUTH_PW'];

# App auth check
if ($client_id == "1471.dvwa.digi.ninja" && $client_secret == "ABigLongSecret") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The login() method only checks $_POST for credentials and grant types. However, the OpenAPI specification (openapi.yml) documents this endpoint as accepting application/json. When clients send JSON payloads, $_POST remains empty, causing the authentication to fail with a "Missing grant type" error. Parse the JSON input from php://input if the content type is application/json.

    private function login() {
        # Default fail, just in case.
        $response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
        $response['body'] = json_encode (array ("status" => "Authentication failed"));

        $params = $_POST;
        if (array_key_exists("CONTENT_TYPE", $_SERVER) && stripos($_SERVER['CONTENT_TYPE'], "application/json") !== false) {
            $json = json_decode(file_get_contents('php://input'), true);
            if (is_array($json)) {
                $params = array_merge($params, $json);
            }
        }

        if (array_key_exists ("PHP_AUTH_USER", $_SERVER) && 
            array_key_exists ("PHP_AUTH_PW", $_SERVER)) {
            $client_id = $_SERVER['PHP_AUTH_USER'];
            $client_secret = $_SERVER['PHP_AUTH_PW'];

            # App auth check
            if ($client_id == "1471.dvwa.digi.ninja" && $client_secret == "ABigLongSecret") {

                if (array_key_exists ("grant_type", $params)) {
                    switch ($params['grant_type']) {
                        case "password":
                            if (array_key_exists ("username", $params) && 
                                array_key_exists ("password", $params)) {
                                $username = $params['username'];
                                $password = $params['password'];

                                if ($username == "mrbennett" && $password == "becareful") {
                                    $response['status_code_header'] = 'HTTP/1.1 200 OK';
                                    $response['body'] = Login::create_token();
                                } else {
                                    $response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
                                    $response['body'] = json_encode (array ("status" => "Invalid user credentials"));
                                }
                            } else {
                                $response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
                                $response['body'] = json_encode (array ("status" => "Missing user credentials"));
                            }
                            break;
                        case "refresh_token":
                            if (array_key_exists ("refresh_token", $params)) {
                                $refresh_token = $params['refresh_token'];

                                # Because this is sent in a POST body, any + characters
                                # get replaced by a space when the URL decode happens. This
                                # puts them back to plus characters.
                                $ref = str_replace (" ", "+", $refresh_token);

                                if (Login::check_refresh_token($ref)) {
                                    $response['status_code_header'] = 'HTTP/1.1 200 OK';
                                    $response['body'] = Login::create_token();
                                } else {
                                    $response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
                                    $response['body'] = json_encode (array ("status" => "Invalid refresh token"));
                                }
                            } else {
                                $response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
                                $response['body'] = json_encode (array ("status" => "Missing refresh token"));
                            }
                            break;
                        default:
                            $response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
                            $response['body'] = json_encode (array ("status" => "Unknown grant type"));
                    }
                } else {
                    $response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
                    $response['body'] = json_encode (array ("status" => "Missing grant type"));
                }
            } else {
                $response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
                $response['body'] = json_encode (array ("status" => "Invalid clientid/clientsecret credentials"));
            }
        } else {
            $response['status_code_header'] = 'HTTP/1.1 401 Unauthorized';
            $response['body'] = json_encode (array ("status" => "Missing clientid/clientsecret credentials"));
        }

Comment on lines +165 to +172
$query = "SELECT COUNT(*) as attempt_count
FROM bac_log
WHERE user_id = ?
AND timestamp > DATE_SUB(NOW(), INTERVAL ? SECOND)";

$stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
if ($stmt) {
mysqli_stmt_bind_param($stmt, "ii", $user_id, $timeframe);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Binding a parameter directly to INTERVAL ? SECOND in a prepared statement is not supported by many versions of MySQL/MariaDB and will result in a SQL syntax error. To ensure maximum compatibility, calculate the cutoff timestamp in PHP and pass it as a standard string parameter.

    $cutoff = date('Y-m-d H:i:s', time() - $timeframe);
    $query = "SELECT COUNT(*) as attempt_count 
              FROM bac_log 
              WHERE user_id = ? 
              AND timestamp > ?";

    $stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
    if ($stmt) {
        mysqli_stmt_bind_param($stmt, "is", $user_id, $cutoff);

Comment on lines +28 to +36
if (is_null ($data)) {
$result = array (
"result" => "fail",
"error" => 'Invalid format, expecting "{id: {user ID}, first_name: "{first name}", surname: "{surname}"}'

);
echo json_encode($result);
exit;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The JSON payload decoded from php://input is not validated to ensure that the required properties (id, first_name, surname) exist. If any of these properties are missing, it will trigger PHP undefined property warnings and result in a malformed SQL query. Validate the presence of these properties before proceeding.

    if (is_null ($data) || !isset($data->id) || !isset($data->first_name) || !isset($data->surname)) {
        $result = array (
                            "result" => "fail",
                            "error" => 'Invalid format, expecting "{id: {user ID}, first_name: "{first name}", surname: "{surname}"}'

                        );
        echo json_encode($result);
        exit;
    }

Comment on lines +5 to +6
error_reporting(E_ALL);
ini_set('display_errors', 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Enabling display_errors in the "impossible" (secure) level is a bad practice as it can lead to Information Disclosure (Full Path Disclosure) if a database or system error occurs. Since the impossible level is meant to demonstrate secure coding best practices, error reporting should be configured to log errors to a secure file instead of displaying them to the user.

error_reporting(E_ALL);
ini_set('display_errors', 0);

@battleops battleops Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ BattleTest Security Review

🚨 REQUEST CHANGES — Risk Score: 100/100 · 11 blocking · 12 advisory (🔴 2 · 🟠 9 · 🟡 9 · 🔵 3 · ⚪ 0)

This PR introduces a new DVWA example application with a composer dependency on swagger-php, but it also adds multiple critical security vulnerabilities including hardcoded default credentials, unsalted MD5 password hashing, and a database credential exposure via a backup config file. The overall posture is extremely risky, and the most critical issue is the hardcoded credentials and weak password storage that allow immediate administrative access and credential compromise.

↳ Top 5 findings posted as inline comments on the diff below.

Important

Actions required outside the codebase:

  • Fixed versions are available for at least one vulnerable dependency. Bump to the versions shown in the inline comments, then re-trigger this review.
  • Immediately delete any existing config.inc.php.bak files from the config/ directory on all deployed instances. Rotate the database password — it has been exposed in a web-accessible location and may already be compromised.
  • After deploying the fix, force all users to reset their passwords. The old MD5 hashes are already weak and may have been exposed.
  • After deployment, immediately change the admin password. The default credentials are publicly documented and widely known.

**18 more findings** (full detail in the [report](https://battletest.dev/reports/36636300-32f7-471d-bb30-07907b0e32c5)):
  • 🟠 High dependenciesCVE-2026-6321 in fast-uri (CVE-2026-6321)
  • 🟠 High dependenciesCVE-2026-6322 in fast-uri (CVE-2026-6322)
  • 🟡 Medium dependenciesCVE-2026-47676 in hono (CVE-2026-47676)
  • 🟡 Medium dependenciesCVE-2026-47675 in hono (CVE-2026-47675)
  • 🟡 Medium dependenciesCVE-2026-44455 in hono (CVE-2026-44455)
  • 🟠 High SQL Injection — PostgreSQL connection string built via string interpolation — potential injection — examples/dvwa/dvwa/includes/DBMS/PGSQL.php:10
  • 🟡 Medium Open Redirect — dvwaPageReload uses HTTP_X_FORWARDED_PREFIX header for redirect — potential open redirect — examples/dvwa/dvwa/includes/dvwaPage.inc.php:130
  • 🟡 Medium Command Injection — shell_exec() used with unsanitized git commands in setup.php — examples/dvwa/setup.php:60
  • 🟡 Medium SQL Injection — PostgreSQL setup script uses string interpolation in SQL queries — examples/dvwa/dvwa/includes/DBMS/PGSQL.php:16
  • 🟡 Medium Information Exposure — PostgreSQL setup script exposes server information via avatar URL construction — examples/dvwa/dvwa/includes/DBMS/PGSQL.php:30
  • 🔵 Low Insecure Cryptography — CSRF token generated using MD5 with weak entropy — examples/dvwa/dvwa/includes/dvwaPage.inc.php:280
  • 🔵 Low Information Exposure — Database credentials displayed on setup status page — examples/dvwa/dvwa/includes/dvwaPage.inc.php:300
  • 🟠 High Open Redirect — Open Redirect via HTTP_X_FORWARDED_PREFIX and PHP_SELF in dvwaPageReload() — examples/dvwa/dvwa/includes/dvwaPage.inc.php:107
  • 🟠 High Stored XSS — Stored XSS via messagesPopAllToHtml() — Session messages rendered without HTML escaping — examples/dvwa/dvwa/includes/dvwaPage.inc.php:196
  • 🟠 High Insecure Session Configuration — Session Cookie Domain Set from Host Header — Cookie Domain Injection — examples/dvwa/dvwa/includes/dvwaPage.inc.php:56
  • 🟡 Medium Weak Cryptography — MD5 Used for CSRF Token Generation — examples/dvwa/dvwa/includes/dvwaPage.inc.php:280
  • 🟡 Medium Broken Access Control — Authentication Bypass via disable_authentication Configuration Flag — examples/dvwa/dvwa/includes/dvwaPage.inc.php:87
  • 🔵 Low Supply Chain / Integrity Verification — Missing SHA integrity checksums (shasum) for all composer packages — examples/dvwa/vulnerabilities/api/composer.lock:1

Vulnerable dependencies:

Recommendations:

  • Add a CI step that runs npm audit or yarn audit on every PR to fail builds when any dependency has a known CVE with severity medium or higher. This would have caught the fast-uri and hono vulnerabilities before merge.
  • Create a centralized configuration validation function (e.g., validateConfig()) that all setup scripts must call before using any user-supplied values in database connections or file operations. This function should sanitize inputs, reject unexpected characters, and enforce allowed patterns — preventing both the SQL injection and credential exposure issues at a structural level.
  • Establish a project-wide policy that all password hashing must use a dedicated library (bcrypt, Argon2) via a shared helper function (e.g., hashPassword()). Add a lint rule or static analysis check that flags any use of md5() or sha1() in authentication-related code, ensuring the weak cryptography finding cannot recur in future changes.
  • Add a pre-commit hook that scans for hardcoded credentials (patterns like password=, pass=, secret=) in configuration files and setup scripts. This would prevent the hardcoded credentials finding from being reintroduced in any new setup or migration code.
  • Implement a redirect whitelist in the application's routing layer: all redirect destinations must match a predefined list of allowed paths or be validated against a strict regex (e.g., only relative paths starting with / and containing no ://). This structural fix addresses the open redirect vulnerability at the framework level rather than per-header.

View full report for the complete breakdown, PoC reproductions, and history.

// Who cares if it fails. Suppress.
@copy($conf, $bakconf);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical (blocking) | attack-chain

Config Backup Exposure → Database Access → MD5 Password Cracking → Full Account Takeover

Why it's dangerous: Complete compromise of all user accounts including admin. The attacker gains persistent authenticated access to the application, can access all vulnerability modules, admin panels, and any data accessible through the application.

Attack chain — click to expand
  1. Attacker navigates to /config/config.inc.php.bak (created automatically by setup.php via copy()) and retrieves database credentials (db_server, db_user, db_password, db_database). [Finding 0]
  2. Attacker connects directly to the MySQL database using the leaked credentials.
  3. Attacker queries the users table and extracts all password hashes, which are stored as unsalted MD5. [Finding 1]
  4. Attacker uses rainbow tables or GPU-based cracking (hashcat) to reverse the MD5 hashes in seconds — the passwords include 'password', 'abc123', 'charley', 'letmein'.
  5. Attacker logs in as any user (including admin) using the cracked plaintext password, gaining full access to all vulnerability pages, admin panels, and user data.

Other links in this chain:

  • examples/dvwa/dvwa/includes/DBMS/MySQL.php:140 — Information Exposure: MySQL.php copies config/config.inc.php to config/config.inc.php.bak, exposing da
  • examples/dvwa/dvwa/includes/DBMS/MySQL.php:62 — Weak Cryptography: User passwords are stored using MD5() which is a fast, unsalted hash that can be

}
else {
$httponly = false;
$samesite = "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical (blocking) | attack-chain

Session Cookie Domain Injection + Open Redirect → Session Hijacking

Why it's dangerous: Complete session hijacking. The attacker gains the victim's authenticated session without needing credentials, bypassing all authentication controls.

Attack chain — click to expand
  1. Attacker crafts a URL to security.php (or setup.php) with a modified Host header (e.g., Host: evil.com) and X-Forwarded-Prefix header (e.g., X-Forwarded-Prefix: https://evil.com). [Finding 12, Finding 4/10]
  2. dvwa_start_session() is called, which uses parse_url($_SERVER['HTTP_HOST'], PHP_URL_HOST) to set the session cookie domain to 'evil.com'. The Set-Cookie header is sent to the victim's browser with domain=evil.com. [Finding 12]
  3. Immediately after, dvwaPageReload() is called, which constructs a redirect URL from HTTP_X_FORWARDED_PREFIX + PHP_SELF, resulting in a redirect to https://evil.com/security.php. [Finding 4/10]
  4. The victim's browser follows the redirect to evil.com and includes the session cookie because the cookie domain matches.
  5. Attacker captures the session cookie on their evil.com server and uses it to impersonate the victim, gaining authenticated access to DVWA.
Other links in this chain (3) — click to expand
  • examples/dvwa/dvwa/includes/dvwaPage.inc.php:56 — Insecure Session Configuration: $_SERVER['HTTP_HOST'] is passed through parse_url() and used as the session cook
  • examples/dvwa/dvwa/includes/dvwaPage.inc.php:130 — Open Redirect: dvwaPageReload() uses the HTTP_X_FORWARDED_PREFIX header value directly in a red
  • examples/dvwa/dvwa/includes/dvwaPage.inc.php:107 — Open Redirect: dvwaPageReload() uses unsanitized $_SERVER['HTTP_X_FORWARDED_PREFIX'] and $_SERV

// Who cares if it fails. Suppress.
@copy($conf, $bakconf);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High (blocking) | Information Exposure | CWE-497

Database credentials exposed via config backup file created during setup

At line 140-143 of MySQL.php, the setup script copies the config file (which contains database credentials including the password) to a .bak file in the same directory. The comment explicitly says 'Copy .bak for a fun directory listing vuln' — this is intentionally creating a vulnerability. If directory listing is enabled or an attacker can guess the path, they can read /config/config.inc.php.bak and obtain the database password and other credentials.

Why it's dangerous: Any attacker who can access /config/config.inc.php.bak can read the database password, username, server, and database name. This gives them direct access to the MySQL database, allowing them to read, modify, or delete all data including user credentials, guestbook entries, and security logs.

Attack chain — click to expand
  1. Attacker navigates to /config/config.inc.php.bak 2. Server returns the config file contents 3. Attacker extracts db_server, db_user, db_password, db_database 4. Attacker connects directly to the MySQL database 5. Attacker reads/exfiltrates all user data including password hashes

Important

Action outside codebase: Immediately delete any existing config.inc.php.bak files from the config/ directory on all deployed instances. Rotate the database password — it has been exposed in a web-accessible location and may already be compromised.

('5','Bob','Smith','smithy',MD5('password'),'{$avatarUrl}smithy.jpg', NOW(), '0');";
if( !mysqli_query($GLOBALS["___mysqli_ston"], $insert ) ) {
dvwaMessagePush( "Data could not be inserted into 'users' table<br />SQL: " . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) );
dvwaPageReload();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High (blocking) | Weak Cryptography | CWE-916

MD5 used for password hashing — weak and unsalted

In MySQL.php lines 62-67, user passwords are inserted using MD5('password'), MD5('abc123'), etc. The login.php also uses md5($pass) to verify passwords. MD5 is a deprecated, fast hash function designed for checksums, not password storage. It lacks a salt, making it trivially vulnerable to rainbow table attacks. Modern GPUs can crack MD5 passwords at billions of hashes per second.

Why it's dangerous: All user passwords (admin/password, gordonb/abc123, 1337/charley, pablo/letmein, smithy/password) are stored as unsalted MD5 hashes. If the database is compromised, these passwords can be cracked in seconds. Since users often reuse passwords, this could lead to compromise of other systems.

Attack chain — click to expand
  1. Attacker gains access to the users table (via SQL injection, config.bak exposure, or other means) 2. Attacker extracts MD5 hashes 3. Attacker uses rainbow tables or GPU cracking (hashcat) to reverse hashes in seconds 4. Attacker logs in as any user using the cracked password

Important

Action outside codebase: After deploying the fix, force all users to reset their passwords. The old MD5 hashes are already weak and may have been exposed.

('5','Bob','Smith','smithy',MD5('password'),'{$avatarUrl}smithy.jpg', NOW(), '0');";
if( !mysqli_query($GLOBALS["___mysqli_ston"], $insert ) ) {
dvwaMessagePush( "Data could not be inserted into 'users' table<br />SQL: " . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) );
dvwaPageReload();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High (blocking) | Hardcoded Credentials | CWE-798

Hardcoded default credentials with known passwords

MySQL.php lines 62-67 insert five user accounts with hardcoded usernames and passwords into the database. The admin account uses password 'password' — one of the most common passwords. All passwords are short, dictionary words that are trivially guessable. These credentials are also documented in the setup.php page which states 'admin // password'.

Why it's dangerous: Anyone who knows about DVWA (or reads the setup page) knows the default admin credentials are admin/password. This gives immediate administrative access to the application, including access to all vulnerability pages, admin panels, and the ability to modify data.

Attack chain — click to expand
  1. Attacker navigates to the login page 2. Attacker tries username 'admin' with password 'password' 3. Attacker gains admin access to the application 4. Attacker accesses all vulnerability pages, admin panels, and user data

Important

Action outside codebase: After deployment, immediately change the admin password. The default credentials are publicly documented and widely known.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant