chore: ensure expected field parsing fails ungracefully
What changed, and why it matters
This commit changes how a wallet app parses invoice data from a partner service. Previously, missing or malformed date/number fields were handled gracefully by returning null or skipping them. Now, those same missing or malformed fields will cause the parsing to crash with an exception. The commit title says this ungraceful failure is intentional. This is a defensive change: it makes the app fail loudly instead of silently accepting bad data, which can prevent subtle bugs or misuse. However, it is not a typical security patch and could in theory be abused if an attacker can feed malformed JSON to the app to trigger a crash (denial of service).
Treat as a hardening/defensive-coding change rather than an active vulnerability fix. Review whether the upstream JSON source can ever be attacker-controlled or malformed in transit; if so, ensure the calling code catches parsing exceptions to avoid crashes. Verify that downstream consumers handle the now-non-nullable expiresAt and externalCustomerKey correctly. No urgent patch action is indicated by the commit alone.
Security signals we found
Fail-fast parsing change: missing or malformed fields now throw instead of being null
Removal of defensive null handling and tryParse helpers
Potential denial-of-service vector if untrusted JSON reaches these factories
No explicit security context, CVE, or vendor security disclosure in commit or references
Evidence from the diff
The diff modifies lib/services/shopinbit/src/models/car_research.dart. It replaces nullable casts and tryParse/tryParse-style helpers with non-nullable casts and DateTime.parse/parse. For example, expiresRaw and createdRaw are now cast to String (not String?), and DateTime.tryParse is replaced with DateTime.parse, which throws on invalid input. externalCustomerKey is changed from String? to required String. A custom toIntOrNull helper is removed, replaced with direct int? casts. A toMap() and toString() are added. The commit message frames this as making expected-field parsing fail ungracefully, i.e., fail fast rather than silently tolerate missing/malformed fields.
Changed components
lib/services/shopinbit/src/models/car_research.dartCarResearchCurrentInvoice.fromJsonCarResearchInvoice.fromJsonCarResearchInvoiceStatus.fromJsonInspect captured patch +30 / −23
diff --git a/lib/services/shopinbit/src/models/car_research.dart b/lib/services/shopinbit/src/models/car_research.dart
index 895b1f1..99501ef 100644
--- a/lib/services/shopinbit/src/models/car_research.dart
+++ b/lib/services/shopinbit/src/models/car_research.dart
@@ -43,16 +43,16 @@ class CarResearchCurrentInvoice {
factory CarResearchCurrentInvoice.fromJson(Map<String, dynamic> json) {
final linksRaw = json['payment_links'] as Map<String, dynamic>? ?? {};
- final expiresRaw = json['expires_at'] as String?;
- final createdRaw = json['created_at'] as String?;
+ final expiresRaw = json['expires_at'] as String;
+ final createdRaw = json['created_at'] as String;
return CarResearchCurrentInvoice(
invoiceId: json['invoice_id'] as String,
status: json['status'] as String,
additional: json['additional'] as String?,
- expiresAt: expiresRaw == null ? null : DateTime.tryParse(expiresRaw),
+ expiresAt: DateTime.parse(expiresRaw),
paymentLinks: linksRaw.map((k, v) => MapEntry(k, v as String)),
hasRequestPayload: json['has_request_payload'] as bool,
- createdAt: createdRaw == null ? null : DateTime.tryParse(createdRaw),
+ createdAt: DateTime.parse(createdRaw),
);
}
}
@@ -82,12 +82,12 @@ bool carResearchIsFinalized(String? status, String? additional) {
class CarResearchInvoice {
final String btcpayInvoice;
- final DateTime? expiresAt;
+ final DateTime expiresAt;
final Map<String, String> paymentLinks;
CarResearchInvoice({
required this.btcpayInvoice,
- this.expiresAt,
+ required this.expiresAt,
required this.paymentLinks,
});
@@ -95,9 +95,7 @@ class CarResearchInvoice {
final linksRaw = json['payment_links'] as Map<String, dynamic>? ?? {};
return CarResearchInvoice(
btcpayInvoice: json['btcpay_invoice'] as String,
- // Null rather than defaulting to now(): a missing/garbled date should
- // not make a fresh invoice look already-expired.
- expiresAt: DateTime.tryParse(json['expires_at']?.toString() ?? ''),
+ expiresAt: DateTime.parse(json['expires_at'] as String),
paymentLinks: linksRaw.map((k, v) => MapEntry(k, v as String)),
);
}
@@ -123,7 +121,7 @@ class CarResearchInvoiceStatus {
final String? receiptTicketNumber;
final int? realTicketId;
final String? realTicketNumber;
- final String? externalCustomerKey;
+ final String externalCustomerKey;
CarResearchInvoiceStatus({
required this.status,
@@ -133,26 +131,35 @@ class CarResearchInvoiceStatus {
this.receiptTicketNumber,
this.realTicketId,
this.realTicketNumber,
- this.externalCustomerKey,
+ required this.externalCustomerKey,
});
factory CarResearchInvoiceStatus.fromJson(Map<String, dynamic> json) {
- int? toIntOrNull(dynamic v) {
- if (v == null) return null;
- if (v is int) return v;
- if (v is double) return v.toInt();
- return int.tryParse(v.toString());
- }
-
return CarResearchInvoiceStatus(
status: json['status'] as String,
additional: json['additional']?.toString(),
finalized: json['finalized'] as bool,
- receiptTicketId: toIntOrNull(json['receipt_ticket_id']),
- receiptTicketNumber: json['receipt_ticket_number']?.toString(),
- realTicketId: toIntOrNull(json['real_ticket_id']),
- realTicketNumber: json['real_ticket_number']?.toString(),
- externalCustomerKey: json['external_customer_key']?.toString(),
+ receiptTicketId: json['receipt_ticket_id'] as int?,
+ receiptTicketNumber: json['receipt_ticket_number'] as String?,
+ realTicketId: json['real_ticket_id'] as int?,
+ realTicketNumber: json['real_ticket_number'] as String?,
+ externalCustomerKey: json['external_customer_key'] as String,
);
}
+
+ Map<String, dynamic> toMap() {
+ return {
+ "status": status,
+ "additional": additional,
+ "finalized": finalized,
+ "receipt_ticket_id": receiptTicketId,
+ "receipt_ticket_number": receiptTicketNumber,
+ "real_ticket_id": realTicketId,
+ "real_ticket_number": realTicketNumber,
+ "external_customer_key": externalCustomerKey,
+ };
+ }
+
+ @override
+ String toString() => toMap().toString();
}
Why this scored 26/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.