X Tutup
Skip to content

Bug: cache.push inside loop in deckService.ts causes getCards() to return mobileapp-only partial results after cache is warmed #2549

@immortal71

Description

@immortal71

Describe the bug

In cornucopia.owasp.org/src/lib/services/deckService.ts, the getCardData() method calls DeckService.cache.push(...) inside the loop that iterates over editions. This means the static cache is populated with a partial result (mobileapp cards only) before the full merged result (mobileapp + webapp) is pushed. Since cache.find() in getCards() returns the first matching entry, every subsequent call to getCards(lang) returns mobileapp-only cards, silently omitting all webapp cards.

Affected file

cornucopia.owasp.org/src/lib/services/deckService.tsgetCardData() method (lines 66–73)

Code snippet (problematic section)

private getCardData(lang: string) {
    let cards = new Map<string, Card>;
    const decks = DeckService.latests;   // [mobileapp 1.1, webapp 2.2]
    for (let i in decks) {
        cards = new Map([...this.getCardDataForEditionVersionLang(decks[i].edition, decks[i].version, lang), ...cards]);
        DeckService.cache.push({ lang: lang, data: cards, version: 'latest' });  // ← BUG: inside loop
    }
    return cards;
}

public getCards(lang: string): Map<string, Card> {
    return DeckService.cache.find((deck) => deck?.lang == lang && deck?.version == 'latest')?.data
        || this.getCardData(lang);
}

Step-by-step execution trace

Iteration decks[i] cards after merge Entry pushed to cache
i = 0 mobileapp 1.1 mobileapp cards only { lang, 'latest', mobileapp-only }first match
i = 1 webapp 2.2 mobileapp + webapp merged { lang, 'latest', full merged }

After the first call to getCardData('en'):

  • cache[0] = mobileapp cards only
  • cache[1] = mobileapp + webapp merged

Any subsequent call to getCards('en'):

  • cache.find(d => d.lang == 'en' && d.version == 'latest') returns cache[0] (first match)
  • Returns mobileapp-only data — webapp cards are completely missing

Expected behavior

cache.push must be placed outside the loop so the cache is only populated with the fully-merged result:

private getCardData(lang: string) {
    let cards = new Map<string, Card>;
    const decks = DeckService.latests;
    for (let i in decks) {
        cards = new Map([...this.getCardDataForEditionVersionLang(decks[i].edition, decks[i].version, lang), ...cards]);
    }
    DeckService.cache.push({ lang: lang, data: cards, version: 'latest' });  // ← moved outside loop
    return cards;
}

Impact

  • The /cards page on cornucopia.owasp.org relies on getCards() to populate the full card browser.
  • After the first page load (which warms the cache), all subsequent calls return only mobileapp cards.
  • Webapp cards (the primary edition) are silently absent from the card browser for all users until the server restarts.

Steps to reproduce

  1. Load a page that calls DeckService.getCards(lang) twice (e.g., navigate to /cards, then navigate away and back).
  2. On the second load, getCards hits the cache and returns only mobileapp cards.
  3. Webapp cards (VE2, AT5, etc.) are absent from the results.

Additional context

  • DeckService.cache is a private static field — a class-level singleton shared across all instances and requests.
  • The bug does not affect direct calls to getCardDataForEditionVersionLang(), only calls going through getCards() after the cache is warmed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      X Tutup