VYPR
Moderate severityNVD Advisory· Published Sep 18, 2023· Updated Sep 24, 2024

Pow Mnesia cache doesn't invalidate all expired keys on startup

CVE-2023-42446

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.

PackageAffected versionsPatched versions
powHex
>= 1.0.14, < 1.0.341.0.34

Affected products

1

Patches

1
15dc525be03c

Merge pull request #714 from pow-auth/fix-cache-invalidation

https://github.com/pow-auth/powDan SchultzerSep 18, 2023via ghsa
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

News mentions

0

No linked articles in our index yet.