CVE-2022-0088 (PHP, published 2022-04-03T09:15Z)
Description: value: Cross-Site Request Forgery (CSRF) in GitHub repository yourls/yourls prior to 1.8.3.
Severity: MEDIUM (CVSS v3 base score: 7.4)
Weakness: CWE-352 \u2014 Cross-Site Request Forgery (CSRF)
The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
Repository: https://github.com/yourls/yourls
Files changed:
.gitignore
admin/admin-ajax.php
includes/functions-auth.php
includes/functions-html.php
tests/tests/auth/login_cookie.php
tests/tests/auth/logout.php
tests/tests/auth/misc.php
tests/tests/auth/nonces.php
Commit message: Add nonce to the logout link (#3264)

* Add nonce to the logout link
* Add tests for cookies being set or reset
* More tests: check nonces are different for different actions & users

Fixes #3170


Vulnerable code:
	case 'logout':
		// unused for the moment
		yourls_logout();
		break;

	if( isset( $_GET['action'] ) && $_GET['action'] == 'logout' ) {
	if( false == $user )
	// get user
	if( false == $user )
		$user = defined( 'YOURLS_USER' ) ? YOURLS_USER : '-1';
	// get current nonce value
	if( false == $nonce && isset( $_REQUEST['nonce'] ) )
		$nonce = $_REQUEST['nonce'];
	$valid = yourls_apply_filter( 'verify_nonce', false, $action, $nonce, $user, $return );
	if ($valid) {
	// what nonce should be
	if( $nonce == $valid ) {
		$logout_link = yourls_apply_filter( 'logout_link', sprintf( yourls__('Hello <strong>%s</strong>'), YOURLS_USER ) . ' (<a href="' . yourls_admin_url( 'index.php' ) . '?action=logout" title="' . yourls_esc_attr__( 'Logout' ) . '">' . yourls__( 'Logout' ) . '</a>)' );
	 * Test login with valid cookie
	 * Test login with invalid cookie
     * Check logout procedure - phase 1
     * Check logout procedure - phase 2
     * Check logout procedure - phase 3
        // $this->assertIsString($url);

Fixed code:
.phpunit.result.cache
 * @return bool|string|mixed true if valid user, error message otherwise. Can also call yourls_die() or redirect to login page. Oh my.
	if( isset( $_GET['action'] ) && $_GET['action'] == 'logout' && isset( $_REQUEST['nonce'] ) ) {
        // The logout nonce is associated to fake user 'logout' since at this point we don't know the real user
        yourls_verify_nonce('admin_logout', $_REQUEST['nonce'], 'logout');
	yourls_do_action( 'pre_setcookie', $user, $time, $path, $domain, $secure, $httponly );

	if( false === $user )
	// Get user
	if( false === $user ) {
        $user = defined('YOURLS_USER') ? YOURLS_USER : '-1';
    }
	// Get nonce value from $_REQUEST if not specified
	if( false === $nonce && isset( $_REQUEST['nonce'] ) ) {
        $nonce = $_REQUEST['nonce'];
    }
	if (yourls_apply_filter( 'verify_nonce', false, $action, $nonce, $user, $return ) === true) {
	// What nonce should be
	if( $nonce === $valid ) {
	    // Create a logout link with a nonce associated to fake user 'logout' : the user is not yet defined
	    // when the logout check is done -- see yourls_is_valid_user()
	    $logout_url = yourls_nonce_url( 'admin_logout',
		yourls_add_query_arg(['action' => 'logout'], yourls_admin_url('index.php')), 'nonce', 'logout');
		$logout_link = yourls_apply_filter('logout_link', sprintf( yourls__('Hello <strong>%s</strong>'), YOURLS_USER ) . ' (<a href="' . $logout_url . '" title="' . yourls_esc_attr__( 'Logout' ) . '">' . yourls__( 'Logout' ) . '</a>)' );
    protected $backup_yourls_actions;
        global $yourls_actions;
        $this->backup_yourls_actions = $yourls_actions;
        global $yourls_actions;
        $yourls_actions = $this->backup_yourls_actions;
	 * Test login with valid cookie - also check that cookie is set
        $this->assertSame( 0, yourls_did_action('pre_setcookie') );
        $this->assertSame( 1, yourls_did_action('pre_setcookie') );
	 * Test login with invalid cookie - also check that no cookie is set
        $this->assertSame( 0, yourls_did_action('pre_setcookie') );
        $this->assertSame( 0, yourls_did_action('pre_setcookie') );
    private static $user;
        self::$user = false;
    public static function setUpBeforeClass(): void {
        yourls_add_action( 'pre_setcookie', function ($in) {
            self::$user = $in[0]; // $in[0] is the user ID passed to yourls_setcookie()
        } );
    }

    public static function tearDownAfterClass(): void {
        yourls_remove_all_actions('pre_setcookie');
    }

     * Check logout procedure - phase 1 - we're logging in
        $this->assertSame(self::$user, 'yourls');
     * Check logout procedure - phase 2 - we're logging out and checking that cookie was reset
        $_REQUEST['nonce'] = yourls_create_nonce('admin_logout', 'logout');
        $this->assertSame(self::$user, null);
     * Check logout procedure - phase 3 - check we can log in again
        $this->assertSame(self::$user, 'yourls');


    /**
     * Check nonces are different for different actions & users
     */
    public function test_nonce_different_for_different_actions_and_users() {
        $action1 = rand_str();
        $action2 = rand_str();
        $user1 = rand_str();
        $user2 = rand_str();

        $nonce_a1 = yourls_create_nonce($action1);
        $nonce_a2 = yourls_create_nonce($action2);
        $nonce_a1_u1 = yourls_create_nonce($action1, $user1);
        $nonce_a1_u2 = yourls_create_nonce($action1, $user2);

        $this->assertNotEquals($nonce_a1, $nonce_a2);
        $this->assertNotEquals($nonce_a1_u1, $nonce_a1_u2);
        $this->assertNotEquals($nonce_a1, $nonce_a1_u1);
    }