CVE-2021-3734: Cross-Site Request Forgery (CSRF) (CWE-352)
value: yourls is vulnerable to Improper Restriction of Rendered UI Layers or Frames
CWE description: 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.
CVSS v3 base score: 8.8
Published: 2021-08-26T13:15Z
Repository: https://github.com/yourls/yourls
Language: PHP
Files changed: admin/admin-ajax.php, includes/functions-auth.php, includes/functions-html.php, includes/functions.php
Commit date: 2021-08-24 18:10:08 +0200
Commit message: Minor security improvements (#3034)

* Prevent iframe jacking
* Enforce nonce on admin login screen
Diff: diff --git a/admin/admin-ajax.php b/admin/admin-ajax.php
index 8dbc0b2..77f68ac 100644
--- a/admin/admin-ajax.php
+++ b/admin/admin-ajax.php
@@ -5,10 +5,11 @@
 yourls_maybe_require_auth();
 
 // This file will output a JSON string
 yourls_content_type_header( 'application/json' );
 yourls_no_cache_headers();
+yourls_no_frame_header();
 
 if( !isset( $_REQUEST['action'] ) )
 	die();
 
 // Pick action
diff --git a/includes/functions-auth.php b/includes/functions-auth.php
index 303d394..d34b3d5 100644
--- a/includes/functions-auth.php
+++ b/includes/functions-auth.php
@@ -120,10 +120,16 @@ function yourls_is_valid_user() {
  * Check auth against list of login=>pwd. Sets user if applicable, returns bool
  *
  */
 function yourls_check_username_password() {
 	global $yourls_user_passwords;
+
+	// If login form (not API), check for nonce
+    if(!yourls_is_API()) {
+        yourls_verify_nonce('admin_login');
+    }
+
 	if( isset( $yourls_user_passwords[ $_REQUEST['username'] ] ) && yourls_check_password_hash( $_REQUEST['username'], $_REQUEST['password'] ) ) {
 		yourls_set_user( $_REQUEST['username'] );
 		return true;
 	}
 	return false;
diff --git a/includes/functions-html.php b/includes/functions-html.php
index 3d68c1d..1cb7fa6 100644
--- a/includes/functions-html.php
+++ b/includes/functions-html.php
@@ -57,10 +57,11 @@ function yourls_html_head( $context = 'index', $title = '' ) {
 	}
 
 	// Force no cache for all admin pages
 	if( yourls_is_admin() && !headers_sent() ) {
         yourls_no_cache_headers();
+        yourls_no_frame_header();
 		yourls_content_type_header( yourls_apply_filter( 'html_head_content-type', 'text/html' ) );
 		yourls_do_action( 'admin_headers', $context, $title );
 	}
 
 	// Store page context
@@ -723,10 +724,11 @@ function yourls_login_screen( $error_msg = '' ) {
 			</p>
 			<?php
 				yourls_do_action( 'login_form_bottom' );
 			?>
 			<p style="text-align: right;">
+			    <?php yourls_nonce_field('admin_login'); ?>
 				<input type="submit" id="submit" name="submit" value="<?php yourls_e( 'Login' ); ?>" class="button" />
 			</p>
 			<?php
 				yourls_do_action( 'login_form_end' );
 			?>
diff --git a/includes/functions.php b/includes/functions.php
index 4ee9ce9..3c755c8 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -274,11 +274,34 @@ function yourls_no_cache_headers() {
         header( 'Pragma: no-cache' );
     }
 }
 
 /**
- * Send a filerable content type header
+ * Send header to prevent display within a frame from another site (avoid clickjacking)
+ *
+ * This header makes it impossible for an external site to display YOURLS admin within a frame,
+ * which allows for clickjacking.
+ * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+ * This said, the whole function is shuntable : legit uses of iframes should be still possible.
+ *
+ * @since 1.8.1
+ * @return void|mixed
+ */
+function yourls_no_frame_header() {
+    // Allow plugins to short-circuit the whole function
+    $pre = yourls_apply_filter( 'shunt_no_frame_header', false );
+    if ( false !== $pre ) {
+        return $pre;
+    }
+
+    if( !headers_sent() ) {
+        header( 'X-Frame-Options: SAMEORIGIN' );
+    }
+}
+
+/**
+ * Send a filterable content type header
  *
  * @since 1.7
  * @param string $type content type ('text/html', 'application/json', ...)
  * @return bool whether header was sent
  */

Vulnerable code:  * Send a filerable content type header
Fixed code: yourls_no_frame_header();

	// If login form (not API), check for nonce
    if(!yourls_is_API()) {
        yourls_verify_nonce('admin_login');
    }

        yourls_no_frame_header();
			    <?php yourls_nonce_field('admin_login'); ?>
 * Send header to prevent display within a frame from another site (avoid clickjacking)
 *
 * This header makes it impossible for an external site to display YOURLS admin within a frame,
 * which allows for clickjacking.
 * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
 * This said, the whole function is shuntable : legit uses of iframes should be still possible.
 *
 * @since 1.8.1
 * @return void|mixed
 */
function yourls_no_frame_header() {
    // Allow plugins to short-circuit the whole function
    $pre = yourls_apply_filter( 'shunt_no_frame_header', false );
    if ( false !== $pre ) {
        return $pre;
    }

    if( !headers_sent() ) {
        header( 'X-Frame-Options: SAMEORIGIN' );
    }
}

/**
 * Send a filterable content type header