Secure Payments Domain Gate

1. Overview

This module implements the Secure Payments domain gate for BastionGuard. Its role is to decide whether a given navigation request must be opened inside the SecureBrowser (CEF sandbox) or delegated to the system browser (gio open / xdg-open), based on a user-managed allowlist stored in payments.json.

The gate is enforced by the SecureList API. It performs:

  • Path resolution for the user configuration file (~/.config/BastionGuard/payments.json)
  • Domain normalization and extraction from URLs
  • Allowlist matching (exact domain and subdomain suffix matching)
  • Runtime enable/disable control through SettingsStore
  • Secure browser launch using SecureBrowser::open()

2. Components and Dependencies

  • SecureBrowser – launches the sandboxed CEF-based browser UI.
  • SettingsStore – provides the runtime toggle for Secure Payments enablement.
  • nlohmann::json – parses payments.json.
  • glibmm – uses Glib::get_user_config_dir() for XDG-compliant config resolution.
  • POSIX / shell tools – invokes gio open (Wayland-friendly) or xdg-open as fallback.

3. Configuration File: payments.json

3.1 Location

The allowlist is read exclusively from the user configuration path:

~/.config/BastionGuard/payments.json

The module intentionally loads only the list field and ignores other metadata keys (e.g., version, enabled, updated_at) to keep the decision logic narrowly scoped and stable.


3.2 Expected Structure

The JSON is expected to be an object containing an array key list:

{
  "list": [
    "paypal.com",
    "stripe.com",
    "example-payment-gateway.tld"
  ]
}

If payments.json is missing, unreadable, invalid, or lacks a valid list array, the allowlist will be treated as empty and Secure Payments decisions will fall back to the system browser.


4. Domain and URL Normalization

4.1 clean_domain()

Domains are normalized through SecureList::clean_domain() to reduce false mismatches and ensure consistent comparison. Normalization rules include:

  • Trim leading/trailing whitespace
  • Force lowercase
  • Remove leading dot (e.g., .paypal.compaypal.com)
  • Strip www. prefix (e.g., www.stripe.comstripe.com)
  • Remove port suffix (e.g., example.com:443example.com)

This function is applied both to entries loaded from payments.json and to runtime extracted hosts.


4.2 extract_domain()

For URL inputs, the module extracts a host component using a permissive regular expression supporting:

  • Optional scheme (e.g., https://)
  • Optional userinfo (e.g., user:pass@)
  • Host extraction without port/path/query/fragment

Extracted values are normalized via clean_domain(). If extraction fails, the module degrades safely by opening the URL in the system browser.


5. Allowlist Loading and Caching

5.1 List-only Loader

The internal loader reads the file and populates a normalized vector of domains from:

  • payments.json["list"] (string entries only)

Non-string entries are skipped. Invalid JSON is handled without crashing; diagnostics are written to stderr.


5.2 Lazy Static Cache

The allowlist is loaded once per process lifecycle through SecureList::payments_cache_only(), which uses:

  • A static boolean init guard
  • A static vector cached holding the normalized allowlist

This approach minimizes IO overhead during frequent URL checks and keeps the hot-path stable. The trade-off is that runtime changes to payments.json will not be picked up until process restart (unless the surrounding application provides a reload mechanism).


6. Matching Logic

6.1 ends_with()

A case-insensitive suffix comparator is provided to support subdomain matching.


6.2 matches_payment_domain()

The decision uses two matching modes against the normalized allowlist:

  • Exact match: host == domain
  • Subdomain match: host ends with "." + domain (e.g., sub.paypal.com matches paypal.com)

This behavior ensures that payment providers and their subdomains remain covered without requiring redundant list entries.


7. Runtime Gate: SettingsStore Integration

7.1 is_payment_domain()

SecureList::is_payment_domain() is the policy entry point. Before any allowlist matching occurs, it verifies that Secure Payments is enabled at runtime:

SettingsStore::get_secure_payments_enabled()

If the setting is disabled, the function returns false immediately, effectively bypassing the Secure Payments gate even if the domain is in the allowlist.


8. Routing Decision API

8.1 open_if_allowed(url, argc, argv)

This is the primary operation exposed by the module. It enforces the following routing flow:

  1. Validate input URL (empty URL is rejected and returns false).
  2. Extract domain via extract_domain().
  3. If domain extraction fails:
    • Open URL in system browser
    • Return false
  4. If domain is allowed by is_payment_domain(domain):
    • Launch SecureBrowser: SecureBrowser::open(url, argc, argv)
    • Return true
  5. Otherwise:
    • Open URL in system browser
    • Return false

The return value communicates whether the secure sandbox path was used (true) or the system browser fallback was used (false).


9. System Browser Fallback

9.1 open_system_browser()

When Secure Payments does not apply, URLs are opened via the system browser using:

  • gio open when a Wayland session is detected (WAYLAND_DISPLAY set) and gio is available.
  • xdg-open as a generic fallback.

This strategy improves compatibility across desktop environments and display servers while keeping dependencies minimal.


10. Operational and Security Considerations

  • Allowlist integrity: only the list field is trusted for decision-making. Invalid or missing lists default to “deny” (system browser fallback).
  • Input normalization: trimming, lowercasing, removal of www. and ports reduces bypass via formatting variations.
  • Subdomain policy: suffix matching allows *.payment-domain.tld by design; ensure allowlist entries are curated to avoid overly broad domains.
  • Cache behavior: allowlist is loaded once per process. If live reload is required, integrate an invalidation strategy (mtime check or explicit reload call).
  • Shell execution: open_system_browser() uses system() with the URL embedded in a quoted string; if URLs can be attacker-controlled, consider migrating to Gio::AppInfo::launch_default_for_uri() or equivalent to avoid shell interpretation.
  • Feature gating: enforcement is controlled by SettingsStore::get_secure_payments_enabled(), ensuring the user can disable Secure Payments without modifying the allowlist file.

11. Typical Integration Flow

  1. User enables Secure Payments in the Settings UI (persisted via SettingsStore).
  2. Application routes sensitive checkout/payment URLs through SecureList::open_if_allowed().
  3. If the domain is allowlisted in payments.json, BastionGuard launches SecureBrowser (CEF sandbox).
  4. Otherwise, the URL is opened via the default system browser for standard browsing.