Pow Mnesia cache doesn't invalidate all expired keys on startup
Description
Pow is a authentication and user management solution for Phoenix and Plug-based apps. Starting in version 1.0.14 and prior to version 1.0.34, use of Pow.Store.Backend.MnesiaCache is susceptible to session hijacking as expired keys are not being invalidated correctly on startup. A session may expire when all Pow.Store.Backend.MnesiaCache instances have been shut down for a period that is longer than a session's remaining TTL. Version 1.0.34 contains a patch for this issue. As a workaround, expired keys, including all expired sessions, can be manually invalidated.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
powHex | >= 1.0.14, < 1.0.34 | 1.0.34 |
Affected products
1Patches
115dc525be03cMerge pull request #714 from pow-auth/fix-cache-invalidation
3 files changed · +42 −34
CHANGELOG.md+8 −0 modified@@ -1,5 +1,13 @@ # Changelog +## v1.0.34 (TBA) + +**Note:** This release contains an important security fix. It is recommended to update immediately if you are using the `Pow.Store.Backend.MnesiaCache`. + +## Bug fixes + +- [`Pow.Store.Backend.MnesiaCache`] Fixed bug where expired cached keys are not invalidated on startup + ## v1.0.33 (2023-09-05) ## Bug fixes
lib/pow/store/backend/mnesia_cache.ex+3 −4 modified@@ -454,10 +454,9 @@ defmodule Pow.Store.Backend.MnesiaCache do :mnesia.foldl(fn {@mnesia_cache_tab, key, {_value, expire}}, invalidators when is_list(key) -> ttl = Enum.max([expire - timestamp(), 0]) - - key - |> unwrap() - |> append_invalidator(invalidators, ttl, config) + [namespace | key] = key + config = Config.put(config, :namespace, namespace) + append_invalidator(key, invalidators, ttl, config) # TODO: Remove by 1.1.0 {@mnesia_cache_tab, key, {_key, _value, _config, expire}}, invalidators when is_binary(key) and is_number(expire) ->
test/pow/store/backend/mnesia_cache_test.exs+31 −30 modified@@ -23,7 +23,7 @@ defmodule Pow.Store.Backend.MnesiaCacheTest do File.rm_rf!("tmp/mnesia") File.mkdir_p!("tmp/mnesia") - start(@default_config) + start() :ok end @@ -34,7 +34,7 @@ defmodule Pow.Store.Backend.MnesiaCacheTest do MnesiaCache.put(@default_config, {"key", "value"}) assert MnesiaCache.get(@default_config, "key") == "value" - restart(@default_config) + restart() assert MnesiaCache.get(@default_config, "key") == "value" @@ -63,7 +63,7 @@ defmodule Pow.Store.Backend.MnesiaCacheTest do assert MnesiaCache.get(@default_config, "key1") == "1" assert MnesiaCache.get(@default_config, "key2") == "2" - restart(@default_config) + restart() assert MnesiaCache.get(@default_config, "key1") == "1" assert MnesiaCache.get(@default_config, "key2") == "2" @@ -85,51 +85,52 @@ defmodule Pow.Store.Backend.MnesiaCacheTest do end test "records auto purge with persistent storage" do - config = Config.put(@default_config, :ttl, 50) + config_1 = Config.put(@default_config, :ttl, 50) + config_2 = Config.put(config_1, :namespace, "other-namespace") - MnesiaCache.put(config, {"key", "value"}) - MnesiaCache.put(config, [{"key1", "1"}, {"key2", "2"}]) + MnesiaCache.put(config_1, {"key", "value"}) + MnesiaCache.put(config_2, [{"key1", "1"}, {"key2", "2"}]) flush_process_mailbox() # Ignore sync write messages - assert MnesiaCache.get(config, "key") == "value" - assert MnesiaCache.get(config, "key1") == "1" - assert MnesiaCache.get(config, "key2") == "2" + assert MnesiaCache.get(config_1, "key") == "value" + assert MnesiaCache.get(config_2, "key1") == "1" + assert MnesiaCache.get(config_2, "key2") == "2" assert_receive {:mnesia_table_event, {:delete, _, _}} # Wait for TTL reached assert_receive {:mnesia_table_event, {:delete, _, _}} # Wait for TTL reached assert_receive {:mnesia_table_event, {:delete, _, _}} # Wait for TTL reached - assert MnesiaCache.get(config, "key") == :not_found - assert MnesiaCache.get(config, "key1") == :not_found - assert MnesiaCache.get(config, "key2") == :not_found + assert MnesiaCache.get(config_1, "key") == :not_found + assert MnesiaCache.get(config_2, "key1") == :not_found + assert MnesiaCache.get(config_2, "key2") == :not_found # After restart - MnesiaCache.put(config, {"key", "value"}) - MnesiaCache.put(config, [{"key1", "1"}, {"key2", "2"}]) + MnesiaCache.put(config_1, {"key", "value"}) + MnesiaCache.put(config_2, [{"key1", "1"}, {"key2", "2"}]) flush_process_mailbox() # Ignore sync write messages - restart(config) - assert MnesiaCache.get(config, "key") == "value" - assert MnesiaCache.get(config, "key1") == "1" - assert MnesiaCache.get(config, "key2") == "2" + restart() + assert MnesiaCache.get(config_1, "key") == "value" + assert MnesiaCache.get(config_2, "key1") == "1" + assert MnesiaCache.get(config_2, "key2") == "2" assert_receive {:mnesia_table_event, {:delete, _, _}} # Wait for TTL reached assert_receive {:mnesia_table_event, {:delete, _, _}} # Wait for TTL reached assert_receive {:mnesia_table_event, {:delete, _, _}} # Wait for TTL reached - assert MnesiaCache.get(config, "key") == :not_found - assert MnesiaCache.get(config, "key1") == :not_found - assert MnesiaCache.get(config, "key2") == :not_found + assert MnesiaCache.get(config_1, "key") == :not_found + assert MnesiaCache.get(config_2, "key1") == :not_found + assert MnesiaCache.get(config_2, "key2") == :not_found # After record expiration updated reschedules - MnesiaCache.put(config, {"key", "value"}) + MnesiaCache.put(config_1, {"key", "value"}) :mnesia.dirty_write({MnesiaCache, ["pow:test", "key"], {"value", :os.system_time(:millisecond) + 150}}) flush_process_mailbox() # Ignore sync write messages assert_receive {:mnesia_system_event, {:mnesia_user, {:reschedule_invalidator, {_, _, _}}}} # Wait for reschedule event - assert MnesiaCache.get(config, "key") == "value" + assert MnesiaCache.get(config_1, "key") == "value" assert_receive {:mnesia_table_event, {:delete, _, _}}, 150 # Wait for TTL reached - assert MnesiaCache.get(config, "key") == :not_found + assert MnesiaCache.get(config_1, "key") == :not_found end test "when initiated with unexpected records" do :mnesia.dirty_write({MnesiaCache, ["pow:test", "key"], :invalid_value}) assert CaptureLog.capture_log([format: "[$level] $message", colors: [enabled: false]], fn -> - restart(@default_config) + restart() end) =~ ~r/\[(warn|warning|)\] #{Regex.escape("Found an unexpected record in the mnesia cache, please delete it: [\"pow:test\", \"key\"]")}/ end @@ -170,16 +171,16 @@ defmodule Pow.Store.Backend.MnesiaCacheTest do end end - defp start(config) do - start_supervised!({MnesiaCache, config}) + defp start do + start_supervised!(MnesiaCache) :mnesia.subscribe(:system) :mnesia.subscribe({:table, MnesiaCache, :simple}) end - defp restart(config) do + defp restart do :ok = stop_supervised(MnesiaCache) :mnesia.stop() - start(config) + start() end describe "distributed nodes" do @@ -773,7 +774,7 @@ defmodule Pow.Store.Backend.MnesiaCacheTest do :stopped = :mnesia.stop() assert CaptureLog.capture_log([format: "[$level] $message", colors: [enabled: false]], fn -> - start(@default_config) + start() end) =~ ~r/\[(warn|warning|)\] #{Regex.escape("Deleting old record in the mnesia cache: \"pow:test:key1\"")}/ assert :mnesia.dirty_read({MnesiaCache, key}) == []
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-3cjh-p6pw-jhv9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-42446ghsaADVISORY
- github.com/pow-auth/pow/commit/15dc525be03c466daa5d2119ca7acdec7b24ed17ghsaWEB
- github.com/pow-auth/pow/issues/713ghsax_refsource_MISCWEB
- github.com/pow-auth/pow/pull/714ghsaWEB
- github.com/pow-auth/pow/security/advisories/GHSA-3cjh-p6pw-jhv9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.