Zum Inhalt springen
Back to the blog
Development
OXID World

Find Users with Weak Password Hashes — and Build Your Own Data Filters — with the OXID Consistency Check Tool

June 10, 2026
7 min read
By Rahat Hameed
Find Users with Weak Password Hashes — and Build Your Own Data Filters — with the OXID Consistency Check Tool

Export by Filter — a generic CSV export with a tag-based extension point. Introduced in v3.0.0.

Building on the image cleanup and SEO URL features shipped in earlier releases, v3.0.0 adds Export by Filter to the OXID Data Consistency Tool. Export by Filter is a generic way to export any subset of shop data to CSV — and a clean extension point so you (or your integrators) can plug in your own filters without forking the component.

The release also ships the first built-in filter: deprecated-credentials, which surfaces every user whose password hash is not native $2y$ Bcrypt.

Why Export by Filter Matters

Most consistency problems in an eShop database don't have a one-size-fits-all command. The questions teams actually ask look like this:

  • Which users are still on a legacy password hash and need a reset?
  • Which customers haven't ordered in three years — candidates for archival under GDPR?
  • Which products were imported from the old PIM and never got their SEO metadata?
  • Every one of those is "find rows matching a rule, export them to CSV, hand them to someone." Until now, each of those checks would have meant either a one-off SQL query against production or a custom console command per question.

    Export by Filter turns that pattern into a first-class extension point: write a small class describing what to export, tag it as a filter, and the tool handles command wiring, CSV generation, timestamped filenames, and logging.

    One command dispatches to whichever filter you name. Registration is a single service tag — nothing else to wire up.

    A Note on the Export Directory

    Required before running in production. By default, app.export_directory_path is export, which puts CSVs at <shop>/source/export/<filename>.csv — inside the web document root. The shop's global source/.htaccess does not deny .csv, so files there are reachable via https://<your-shop>/export/<filename>.csv. The consistency-check CSVs are privacy-minimal, but they still list accounts plus activity dates.

    Before running export commands in production, either:

  • Override app.export_directory_path to an absolute path outside the document root (e.g. /var/log/oxid/exports), or
  • Drop a deny-all .htaccess into source/export/.
  • The README shows both options in full.

    A Closer Look: the export-by-filter Command

    Version 3.0.0 introduces a single new command, oe:consistency_check:export-by-filter, which dispatches to whichever filter you name:

    vendor/bin/oe-console oe:consistency_check:export-by-filter --filter-name=<filter>

    Output lands in export/<filter-name>-<timestamp>.csv — for example export/deprecated-credentials-2026-06-03_14-30-15.csv (the directory is configurable — see the security note above).

    The First Built-in Filter: deprecated-credentials

    OXID shops accumulate password hashes from many sources over their lifetime — SHA512 from older shop versions, MD5 from very old or hand-imported users, Bcrypt variants from non-PHP backends, and occasionally custom plugins. From a security standpoint these are not equivalent, and the first step to fixing them is knowing where they are.

    deprecated-credentials exports every user whose password hash is not native $2y$ Bcrypt and is not empty:

    vendor/bin/oe-console oe:consistency_check:export-by-filter --filter-name=deprecated-credentials
    Each hash is classified by its prefix. Native $2y$ and empty (not_set) hashes are skipped; everything else is exported — no direct PII, just the user identifier plus activity metadata.

    The CSV includes:

  • user_id: User OXID (database key only — no email or name)
  • active: Whether the account is active
  • created_at: Account creation date
  • user_updated_at: Last modification of the user row (account activity signal — not "password last changed")
  • last_order_at: Date of last order, or empty
  • credential_status: Hash classification (see below)
  • credential_hash_scheme: Detected algorithm (bcrypt, sha512, md5, unknown)
  • credential_status values

  • deprecated: SHA512 — older shop hashing scheme. Recommended action: force password reset / re-hash on next login.
  • unsupported: MD5 — very old, broken. Recommended action: force password reset.
  • supported: Bcrypt $2a$ / $2b$ — externally produced (Java BCrypt, OpenBSD, modern non-PHP libraries, manual imports). Cryptographically equivalent to $2y$. No action needed — informational only.
  • unknown: Any other format — argon2, scrypt, custom hash plugin, garbled data. Recommended action: investigate manually.
  • not_set: Empty password hash. Filtered out at the database level; never appears in the export.
  • Note: $2a$/$2b$ rows are exported deliberately so the CSV is a complete audit of non-$2y$ hashes; filter them out client-side if you only want actionable rows.

    Privacy by Design

    The CSV contains no direct PII — no email, no name, no address — only the user identifier plus the activity metadata needed to prioritise each row (account state, creation date, last modification, last order date, and the hash classification). That's deliberate: the file can be shared, archived, or attached to a ticket without leaking PII. To contact the affected users, look up OXUSERNAME from oxuser separately via the user_id column.

    Subshop Scope

    In CE/PE (single-shop) installations the export is naturally shop-local; the rest of this section describes the Enterprise Edition behaviour, where subshops share the user table. The export covers users from every shop in an Enterprise installation, with no shop-id column. For blMallUsers = true setups this matches the auth model: a single user record authenticates against any shop in the mall, so a weak hash is a mall-installation-wide concern. For blMallUsers = false setups the user → shop assignment is local; partners running single-shop or strictly separated mall configurations may want a per-shop slice — easy to do with a custom filter (see below).

    Extending: Build Your Own Filter

    This is the core of the release. Every shop has its own definition of "the rows I need to see." Rather than baking those definitions into the tool, 3.0.0 ships a tag-based extension point so any component — yours, an integrator's, or a one-off project package — can register a filter and immediately get a working export-by-filter --filter-name=... invocation.

    Step 1: Implement FilterInterface

    Create a class in your component that implements OxidEsales\ConsistencyCheck\ExportByFilter\Filter\FilterInterface. The interface defines four methods:

  • getName() — the filter name (the value passed to --filter-name)
  • getHeaders() — the CSV header row
  • getItems() — the rows to export, returned as an array of DTOs
  • getArrayFactory() — the ArrayFactory that converts each DTO into a CSV row array
  • The interface lives at src/ExportByFilter/Filter/FilterInterface.php — read it once and you've seen the whole contract.

    Step 2: Register the Filter Service

    Add your filter to your component's services.yaml with the oe.consistency_check.export_filter tag:

    services:
    YourVendor\YourComponent\Filter\InactiveCustomersFilter:
    autowire: true
    tags: ['oe.consistency_check.export_filter']

    The tag is the entire registration — no central registry to edit, no command to subclass.

    Step 3: Run It

    vendor/bin/oe-console oe:consistency_check:export-by-filter --filter-name=inactive-customers

    For a complete worked example, the DeprecatedCredentialsFilter class in src/ExportByFilter/Filter/Credentials/ is intentionally written as a reference implementation — same shape as anything you'd write yourself.

    Filter Ideas

    A few directions where a custom filter pays for itself almost immediately:

  • Inactive customers — users with no orders in N months, for GDPR-driven retention reviews
  • Orphaned addresses — oxaddress rows pointing at deleted users
  • Products without images — articles where every OXPIC* field is empty
  • Cross-shop SKU collisions — articles sharing an OXARTNUM across subshops where they shouldn't
  • Stale inventory — products with OXSTOCK > 0 but no sales in the last year
  • None of these need a new command. They're all "implement FilterInterface, tag the service, done."

    Compatibility and Requirements

  • OXID eShop Compilation: 7.5
  • PHP: 8.3 minimum, tested up to 8.5
  • Installation

    To get started, install or update the tool via Composer:

    composer require oxid-esales/consistency-check-tool

    Repository: github.com/OXID-eSales/consistency-check-tool

    Looking Ahead

    The OXID Data Consistency Tool continues to evolve. Image cleanup gave us a safe pattern for modifying shop data. SEO URL management generalized that into a check-export-review-delete workflow. Export by Filter takes the next step: instead of shipping one command per question, it gives shops and integrators a stable extension point so the next consistency check can live in a component — not in a fork.

    Other features in the tool — image cleanup, SEO URL management — follow the same pattern: dry-run, CSV-based, privacy-minimal.

    Feedback

    Open an issue if a filter you'd like to see in-box is missing, or contribute one through a PR.

    GitHub Repository: github.com/OXID-eSales/consistency-check-tool