PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.8
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | includes/class-database.php +8 -236 3.0.02.11.8 View file →
@@ -1493,152 +1493,13 @@
1493 1493 * @param int $user_id User ID.
1494 1494 * @return array|null
1495 1495 */
1496 1496 public function get_totp_data( $user_id ) {
1497 - return $this->get_totp_row( $this->totp_table_for_user( $user_id ), $user_id );
1498 - }
1497 + $table = $this->get_totp_table();
1499 1498
1500 - /**
1501 - * The TOTP table that holds this account's enrolment
1502 - *
1503 - * On a network the enrolment has to be visible wherever the login arrives.
1504 - * This table carries the blog prefix, so an account enrolled on the main site
1505 - * had no row on a subsite: the "not set up yet" branch of
1506 - * check_2fa_requirement() let the login through and even wrote a grace
1507 - * placeholder there, and the cookie that came out was valid across the whole
1508 - * network.
1509 - *
1510 - * Every read AND every write goes through here, which is the part the first
1511 - * attempt got wrong: falling back only on the read meant verify_backup_code()
1512 - * found the code on the main site and then wrote the shortened list to the
1513 - * local table, where there is no row. wpdb::update() touches nothing, returns
1514 - * 0, and the caller reads that as success, so a single-use backup code stayed
1515 - * usable for ever. Found by the cross review of 2.11.10.
1516 - *
1517 - * A configured local row always wins, so an enrolment made on a subsite is
1518 - * never lost or overwritten.
1519 - *
1520 - * The search does not stop at the main site, and that is the second
1521 - * correction: looking only local then main left an account enrolled on one
1522 - * subsite reading as "not set up yet" from the main site and from every
1523 - * other subsite, which is the very branch that lets the login through. It
1524 - * did not show while the email class was registering on every network and
1525 - * catching those logins, and it would have become a live bypass the moment
1526 - * that was put right, which is exactly what this release also does. The two
1527 - * were found together by the second cross review of 2.11.10 and are fixed
1528 - * together on purpose: fixing one alone opens the other.
1529 - *
1530 - * @since 2.11.10
1531 - *
1532 - * @param int $user_id User ID.
1533 - * @return string Table name.
1534 - */
1535 - private function totp_table_for_user( $user_id ) {
1536 - $local = $this->get_totp_table();
1537 -
1538 - if ( ! is_multisite() ) {
1539 - return $local;
1540 - }
1541 -
1542 - $row = $this->get_totp_row( $local, $user_id );
1543 -
1544 - if ( $row && ! empty( $row['is_configured'] ) ) {
1545 - return $local;
1546 - }
1547 -
1548 - /*
1549 - * Where the enrolment is, asked of the account itself. User meta is
1550 - * network-global, so this one row answers from any site of the network,
1551 - * whatever its size and whoever the account is.
1552 - */
1553 - $marked = (int) get_user_meta( $user_id, 'vigilante_totp_site', true );
1554 -
1555 - if ( $marked > 0 ) {
1556 - $table = $this->wpdb->get_blog_prefix( $marked ) . $this->two_factor_totp_table;
1557 - $candidate = ( $table === $local ) ? $row : $this->get_totp_row( $table, $user_id );
1558 -
1559 - if ( $candidate && ! empty( $candidate['is_configured'] ) ) {
1560 - return $table;
1561 - }
1562 - }
1563 -
1564 - /*
1565 - * No marker: an enrolment made before this version, or one whose site is
1566 - * gone. Searched once, the old way, and written down when found so the
1567 - * search never happens again for this account.
1568 - */
1569 - foreach ( $this->totp_legacy_blog_ids( $user_id ) as $blog_id ) {
1570 - $table = $this->wpdb->get_blog_prefix( $blog_id ) . $this->two_factor_totp_table;
1571 -
1572 - if ( $table === $local ) {
1573 - continue;
1574 - }
1575 -
1576 - $candidate = $this->get_totp_row( $table, $user_id );
1577 -
1578 - if ( $candidate && ! empty( $candidate['is_configured'] ) ) {
1579 - $this->remember_totp_site( $user_id, (int) $blog_id );
1580 - return $table;
1581 - }
1582 - }
1583 -
1584 - return $local;
1585 - }
1586 -
1587 - /**
1588 - * Where an enrolment made before the marker existed could live
1589 - *
1590 - * Only for accounts with no vigilante_totp_site meta yet, and only until the
1591 - * first time one is found, because finding it writes the marker. The main
1592 - * site first, since that is where a network that configures two factor once
1593 - * sets it up, then the sites the account belongs to, then the rest of the
1594 - * network for a super administrator, who is asked for a second factor by
1595 - * every site and is a member of almost none.
1596 - *
1597 - * @since 2.11.10
1598 - *
1599 - * @param int $user_id User ID.
1600 - * @return int[] Blog IDs.
1601 - */
1602 - private function totp_legacy_blog_ids( $user_id ) {
1603 - $ids = array( (int) get_main_site_id() );
1604 -
1605 - foreach ( get_blogs_of_user( $user_id ) as $blog ) {
1606 - $ids[] = (int) $blog->userblog_id;
1607 - }
1608 -
1609 - if ( is_super_admin( $user_id ) ) {
1610 - foreach ( get_sites( array( 'fields' => 'ids', 'number' => 200 ) ) as $blog_id ) {
1611 - $ids[] = (int) $blog_id;
1612 - }
1613 - }
1614 -
1615 - return array_values( array_unique( $ids ) );
1616 - }
1617 -
1618 - /**
1619 - * One TOTP row from a given table
1620 - *
1621 - * @since 2.11.10
1622 - *
1623 - * @param string $table Table name.
1624 - * @param int $user_id User ID.
1625 - * @return array|null
1626 - */
1627 - private function get_totp_row( $table, $user_id ) {
1628 - /*
1629 - * The table asked for may not exist: on a network Vigilant can have been
1630 - * activated on some sites and not on others, and this is called with the
1631 - * candidate tables of every site the account belongs to. Asking for a
1632 - * table that is not there would print a database error on the login
1633 - * page, so errors are silenced for the duration and a missing table
1634 - * reads as what it means, no enrolment there.
1635 - */
1636 - $suppress = $this->wpdb->suppress_errors( true );
1637 -
1638 1499 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- %i placeholder requires WP 6.2+.
1639 1500 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1640 - $row = $this->wpdb->get_row(
1501 + return $this->wpdb->get_row(
1641 1502 $this->wpdb->prepare(
1642 1503 'SELECT * FROM %i WHERE user_id = %d LIMIT 1',
1643 1504 $table,
1644 1505 $user_id
@@ -1645,12 +1506,8 @@
1645 1506 ),
1646 1507 ARRAY_A
1647 1508 );
1648 1509 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
1649 -
1650 - $this->wpdb->suppress_errors( $suppress );
1651 -
1652 - return $row;
1653 1510 }
1654 1511
1655 1512 /**
1656 1513 * Create TOTP placeholder row (grace period tracking)
@@ -1659,9 +1516,9 @@
1659 1516 * @param string $grace_expires Grace period expiry datetime.
1660 1517 * @return bool
1661 1518 */
1662 1519 public function create_totp_placeholder( $user_id, $grace_expires ) {
1663 - $table = $this->totp_table_for_user( $user_id );
1520 + $table = $this->get_totp_table();
1664 1521
1665 1522 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1666 1523 return false !== $this->wpdb->replace(
1667 1524 $table,
@@ -1682,13 +1539,13 @@
1682 1539 * @param string $encrypted Encrypted secret.
1683 1540 * @return bool
1684 1541 */
1685 1542 public function save_totp_data( $user_id, $encrypted ) {
1686 - $table = $this->totp_table_for_user( $user_id );
1543 + $table = $this->get_totp_table();
1687 1544 $now = current_time( 'mysql', true );
1688 1545
1689 1546 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
1690 - $saved = false !== $this->wpdb->replace(
1547 + return false !== $this->wpdb->replace(
1691 1548 $table,
1692 1549 array(
1693 1550 'user_id' => $user_id,
1694 1551 'secret' => $encrypted,
@@ -1697,92 +1554,11 @@
1697 1554 'grace_period_expires' => null,
1698 1555 ),
1699 1556 array( '%d', '%s', '%d', '%s', '%s' )
1700 1557 );
1701 -
1702 - if ( $saved ) {
1703 - $this->remember_totp_site( $user_id, $this->blog_id_of_totp_table( $table ) );
1704 - }
1705 -
1706 - return $saved;
1707 1558 }
1708 1559
1709 1560 /**
1710 - * Whether this account has an enrolment anywhere in the network
1711 - *
1712 - * With the account's vigilante_totp_site marker in place this is one or two
1713 - * queries: the local table, and the one the marker points at. Without it,
1714 - * which is every account with no enrolment at all, totp_table_for_user() goes
1715 - * on to search the main site, the account's own sites and, for a super
1716 - * administrator, up to 200 more, and it does so every time, because the
1717 - * marker is only written once an enrolment is found. It runs at login and,
1718 - * since 2.11.10, from the dashboard hooks of the TOTP class too; that is why
1719 - * those ask for the election before reading the row (2.11.11).
1720 - *
1721 - * @since 2.11.10
1722 - *
1723 - * @param int $user_id User ID.
1724 - * @return bool
1725 - */
1726 - public function has_totp_enrolment( $user_id ) {
1727 - $row = $this->get_totp_data( $user_id );
1728 -
1729 - return ( $row && ! empty( $row['is_configured'] ) );
1730 - }
1731 -
1732 - /**
1733 - * The blog a TOTP table belongs to
1734 - *
1735 - * @since 2.11.10
1736 - *
1737 - * @param string $table Table name.
1738 - * @return int Blog ID, 0 when it cannot be told.
1739 - */
1740 - private function blog_id_of_totp_table( $table ) {
1741 - if ( ! is_multisite() ) {
1742 - return 0;
1743 - }
1744 -
1745 - $base = $this->wpdb->base_prefix . $this->two_factor_totp_table;
1746 -
1747 - if ( $table === $base ) {
1748 - return (int) get_main_site_id();
1749 - }
1750 -
1751 - if ( 1 === preg_match( '/^' . preg_quote( $this->wpdb->base_prefix, '/' ) . '(\d+)_' . preg_quote( $this->two_factor_totp_table, '/' ) . '$/', $table, $m ) ) {
1752 - return (int) $m[1];
1753 - }
1754 -
1755 - return 0;
1756 - }
1757 -
1758 - /**
1759 - * Write down which site holds this account's enrolment
1760 - *
1761 - * User meta is network-global, so one row says where the enrolment is from
1762 - * anywhere. That is what makes the lookup exact instead of a search: the
1763 - * first version walked the main site plus the account's own sites, capped at
1764 - * 25, and any enrolment outside that set read as "not set up yet", which is
1765 - * the branch that lets the login through. The third cross review of 2.11.10
1766 - * measured all three ways out of it: a network with more sites than the cap,
1767 - * an enrolment on a site the account was later removed from, and a super
1768 - * administrator, who is asked for a second factor by every site of the
1769 - * network and is a member of almost none.
1770 - *
1771 - * @since 2.11.10
1772 - *
1773 - * @param int $user_id User ID.
1774 - * @param int $blog_id Blog the enrolment was written to.
1775 - */
1776 - private function remember_totp_site( $user_id, $blog_id ) {
1777 - if ( ! is_multisite() || $blog_id < 1 ) {
1778 - return;
1779 - }
1780 -
1781 - update_user_meta( $user_id, 'vigilante_totp_site', (int) $blog_id );
1782 - }
1783 -
1784 - /**
1785 1561 * Store backup codes for a user
1786 1562 *
1787 1563 * @param int $user_id User ID.
1788 1564 * @param string $hashed_codes JSON-encoded hashed codes.
@@ -1788,9 +1564,9 @@
1788 1564 * @param string $hashed_codes JSON-encoded hashed codes.
1789 1565 * @return bool
1790 1566 */
1791 1567 public function store_totp_backup_codes( $user_id, $hashed_codes ) {
1792 - $table = $this->totp_table_for_user( $user_id );
1568 + $table = $this->get_totp_table();
1793 1569
1794 1570 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1795 1571 return false !== $this->wpdb->update(
1796 1572 $table,
@@ -1807,9 +1583,9 @@
1807 1583 * @param int $user_id User ID.
1808 1584 * @return bool
1809 1585 */
1810 1586 public function update_totp_last_used( $user_id ) {
1811 - $table = $this->totp_table_for_user( $user_id );
1587 + $table = $this->get_totp_table();
1812 1588 $now = current_time( 'mysql', true );
1813 1589
1814 1590 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1815 1591 return false !== $this->wpdb->update(
@@ -1827,13 +1603,9 @@
1827 1603 * @param int $user_id User ID.
1828 1604 * @return bool
1829 1605 */
1830 1606 public function reset_totp_data( $user_id ) {
1831 - $table = $this->totp_table_for_user( $user_id );
1832 -
1833 - // The marker goes with the row it points at, or the next login would look
1834 - // for an enrolment that is no longer there.
1835 - delete_user_meta( $user_id, 'vigilante_totp_site' );
1607 + $table = $this->get_totp_table();
1836 1608
1837 1609 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1838 1610 return false !== $this->wpdb->delete(
1839 1611 $table,