Automattic\WooCommerce\Blueprint\Importers

ImportRunSql{} │ WC 1.0└─ StepProcessor

Processes SQL execution steps in the Blueprint.

This step executes SQL supplied by the imported file against the site's database. Importing a Blueprint that contains it is equivalent to letting the author of that file run queries on the store, and the only access control is check_step_capabilities() below.

The checks in this class inspect the query as text, so they cannot decide what a statement will actually do: the same write can be expressed in forms the patterns here do not match. They exist to catch mistakes and casual misuse and must not be relied on as a security boundary — do not extend them in the belief that enough patterns will make them one.

The boundary is the decision to import the file. That decision is where the control belongs, so the import screen lists what a Blueprint will do — including how many queries it runs — before any step executes, and states that a Blueprint should only be imported from a trusted source. Anything that weakens that disclosure weakens the actual protection here.

No Hooks.

Usage

$ImportRunSql = new ImportRunSql();
// use class methods

Methods

  1. public check_step_capabilities( $schema )
  2. public get_step_class()
  3. public process( $schema )
  4. private affects_protected_tables( string $sql_content )
  5. private affects_user_capabilities( string $sql_content )
  6. private contains_sql_injection_patterns( string $sql_content )
  7. private contains_suspicious_comments( string $sql_content )
  8. private is_allowed_query_type( string $sql_content )

Notes

Package: Automattic\WooCommerce\Blueprint\Importers

ImportRunSql{} code WC 11.1.2

class ImportRunSql implements StepProcessor {
	use UsePluginHelpers;
	use UseWPFunctions;

	/**
	 * List of allowed SQL query types.
	 *
	 * @var array
	 */
	private const ALLOWED_QUERY_TYPES = array(
		'INSERT',
		'UPDATE',
		'REPLACE INTO',
	);


	/**
	 * Process the SQL execution step.
	 *
	 * Runs the text-level checks — statement type, comment patterns, injection
	 * patterns, protected tables and capability-related option rows — and then
	 * executes the query in a transaction. See the class docblock for what those
	 * checks are and are not: they reject recognisable misuse, they do not
	 * constrain a determined author of the imported file.
	 *
	 * @param object $schema The schema containing the SQL query to execute.
	 * @return StepProcessorResult The result of the SQL execution.
	 */
	public function process( $schema ): StepProcessorResult {
		global $wpdb;
		$result = StepProcessorResult::success( RunSql::get_step_name() );

		$sql = trim( $schema->sql->contents );
		$sql = str_replace( RunSql::TABLE_PREFIX_PLACEHOLDER, $wpdb->prefix, $sql );

		// Check if the query type is allowed.
		if ( ! $this->is_allowed_query_type( $sql ) ) {
			$result->add_error(
				sprintf(
					'Only %s queries are allowed.',
					implode( ', ', self::ALLOWED_QUERY_TYPES )
				)
			);
			return $result;
		}

		// Check for SQL comments that might be hiding malicious code.
		if ( $this->contains_suspicious_comments( $sql ) ) {
			$result->add_error( 'SQL query contains suspicious comment patterns.' );
			return $result;
		}

		// Detect SQL injection patterns.
		if ( $this->contains_sql_injection_patterns( $sql ) ) {
			$result->add_error( 'SQL query contains potential injection patterns.' );
			return $result;
		}

		// Check if the query affects protected tables.
		if ( $this->affects_protected_tables( $sql ) ) {
			$result->add_error( 'Modifications to admin users or roles are not allowed.' );
			return $result;
		}

		// Check if the query affects user capabilities in wp_options.
		if ( $this->affects_user_capabilities( $sql ) ) {
			$result->add_error( 'Modifications to user roles or capabilities are not allowed.' );
			return $result;
		}

		$wpdb->suppress_errors( true );
		$wpdb->query( 'START TRANSACTION' );

		try {
			$query_result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			$last_error = $wpdb->last_error;
			if ( $last_error ) {
				$wpdb->query( 'ROLLBACK' );
				$result->add_error( 'Error executing SQL: ' . $last_error );
			} else {
				$wpdb->query( 'COMMIT' );
				$result->add_debug( "Executed SQL ({$schema->sql->name}): Affected {$query_result} rows" );
			}
		} catch ( \Throwable $e ) {
			$wpdb->query( 'ROLLBACK' );
			$result->add_error( "Exception executing SQL: {$e->getMessage()}" );
		}

		return $result;
	}

	/**
	 * Returns the class name of the step this processor handles.
	 *
	 * @return string The class name of the step this processor handles.
	 */
	public function get_step_class(): string {
		return RunSql::class;
	}

	/**
	 * Check if the current user has the required capabilities for this step.
	 *
	 * @param object $schema The schema to process.
	 *
	 * @return bool True if the user has the required capabilities. False otherwise.
	 */
	public function check_step_capabilities( $schema ): bool {
		if ( ! current_user_can( 'manage_options' ) ) {
			return false;
		}

		if ( ! current_user_can( 'edit_posts' ) ) {
			return false;
		}

		if ( ! current_user_can( 'edit_users' ) ) {
			return false;
		}

		return true;
	}
	/**
	 * Check if the SQL query type is allowed.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if the query type is allowed, false otherwise.
	 */
	private function is_allowed_query_type( string $sql_content ): bool {
		$uppercase_sql_content = strtoupper( trim( $sql_content ) );

		foreach ( self::ALLOWED_QUERY_TYPES as $query_type ) {
			if ( 0 === stripos( $uppercase_sql_content, $query_type ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Check for suspicious comment patterns that might hide malicious code.
	 *
	 * This method detects various types of SQL comments that might be used
	 * to hide malicious SQL commands or bypass security filters.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if suspicious comments found, false otherwise.
	 */
	private function contains_suspicious_comments( string $sql_content ): bool {
		// Quick check if there are any comments at all before running regex.
		if (
			strpos( $sql_content, '--' ) === false &&
			strpos( $sql_content, '/*' ) === false &&
			strpos( $sql_content, '#' ) === false
		) {
			return false;
		}

		// List of potentially dangerous SQL commands to check for in comments.
		$dangerous_commands = array(
			'DELETE',
			'DROP',
			'ALTER',
			'CREATE',
			'TRUNCATE',
			'GRANT',
			'REVOKE',
			'EXEC',
			'EXECUTE',
			'CALL',
			'INTO OUTFILE',
			'INTO DUMPFILE',
			'LOAD_FILE',
			'LOAD DATA',
			'BENCHMARK',
			'SLEEP',
			'INFORMATION_SCHEMA',
			'USER\\(',
			'DATABASE\\(',
			'SCHEMA\\(',
		);

		$dangerous_pattern = implode( '|', $dangerous_commands );

		// Check for SQL comments that might be hiding malicious code.
		$patterns = array(
			// Single-line comments (-- style) containing dangerous commands.
			'/--.*?(' . $dangerous_pattern . ')/i',
			// Single-line comments (# style) containing dangerous commands.
			'/#.*?(' . $dangerous_pattern . ')/i',
			// Multi-line comments hiding dangerous commands.
			'/\/\*.*?(' . $dangerous_pattern . ').*?\*\//is',
			// MySQL-specific execution comments (version-specific code execution).
			'/\/\*![0-9]*.*?\*\//',
		);

		foreach ( $patterns as $pattern ) {
			if ( preg_match( $pattern, $sql_content ) ) {
				return true;
			}
		}
		return false;
	}


	/**
	 * Check for common SQL injection patterns.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if potential injection patterns found, false otherwise.
	 */
	private function contains_sql_injection_patterns( string $sql_content ): bool {
		$patterns = array(
			'/UNION\s+(?:ALL\s+)?SELECT/i',  // UNION-based injections.
			'/OR\s+1\s*=\s*1/i',             // OR 1=1 condition.
			'/AND\s+0\s*=\s*0/i',            // AND 0=0 condition.
			'/;\s*--/i',                     // Inline comment terminations.
			'/SLEEP\s*\(/i',                 // Time-based injections.
			'/BENCHMARK\s*\(/i',             // Benchmark-based injections.
			'/LOAD_FILE\s*\(/i',             // File access.
			'/INTO\s+OUTFILE/i',             // File write.
			'/INTO\s+DUMPFILE/i',            // File dump.
			'/CREATE\s+(?:TEMPORARY\s+)?TABLE/i',  // Table creation.
			'/DROP\s+TABLE/i',               // Table deletion.
			'/ALTER\s+TABLE/i',              // Table alteration.
			'/INFORMATION_SCHEMA/i',         // Database metadata access.
			'/EXEC\s*\(/i',                  // Stored procedure execution.
			'/SCHEMA_NAME/i',                // Schema access.
			'/DATABASE\(\)/i',               // Current database name.
			'/CHR\s*\(/i',                   // Character function for evasion.
			'/CHAR\s*\(/i',                  // Character function for evasion.
			'/FROM\s+mysql\./i',             // Direct MySQL system table access.
			'/FROM\s+information_schema\./i', // Direct information schema access.
		);
		foreach ( $patterns as $pattern ) {
			if ( preg_match( $pattern, $sql_content ) ) {
				return true;
			}
		}

		return false;
	}


	/**
	 * Check if the SQL query affects protected user tables.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if the query affects protected tables, false otherwise.
	 */
	private function affects_protected_tables( string $sql_content ): bool {
		global $wpdb;
		$protected_tables = array(
			$wpdb->users,
			$wpdb->usermeta,
		);

		foreach ( $protected_tables as $table ) {
			if ( preg_match( '/\b' . preg_quote( $table, '/' ) . '\b/i', $sql_content ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if the SQL query affects user capabilities in wp_options.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if the query affects user capabilities, false otherwise.
	 */
	private function affects_user_capabilities( string $sql_content ): bool {
		global $wpdb;

		// Check if the query affects user capabilities in wp_options.
		if ( stripos( $sql_content, $wpdb->prefix . 'options' ) !== false ) {
			$option_patterns = array(
				'user_roles',
				'capabilities',
				'wp_user_',
				'role_',
				'administrator',
			);

			foreach ( $option_patterns as $pattern ) {
				if ( stripos( $sql_content, $pattern ) !== false ) {
					return true;
				}
			}
		}
		return false;
	}
}