I review a fair amount of other developers' Acumatica customizations before they go into production on client instances, and the same handful of mistakes show up across completely unrelated projects. None of them are exotic. They're the ordinary web application security basics that get skipped because Acumatica's framework does so much for you by default that it's easy to forget the few places it doesn't, and assume everything is covered.
BQL parameterizes for you - don't defeat it with string concatenation
BQL queries are parameterized by construction; the injection risk essentially disappears as long as you use BQL as intended. The vulnerability shows up specifically when a developer drops down to raw SQL via PXDatabase.SelectMultiple or a direct SqlCommand and builds the query with string concatenation, usually to work around a BQL limitation they didn't fully understand:
// Never do this - classic SQL injection, and I still find it in the wild
string sql = "SELECT * FROM ARInvoice WHERE CustomerID = '" + custId + "'";
// If raw SQL is genuinely unavoidable, parameterize it explicitly
var cmd = new PXSPParameter[] { new PXSPParameter("@custId", PXDbType.NVarChar, custId) };
Almost every case I've found this in turned out to have a perfectly good BQL equivalent the original developer just hadn't discovered - a dynamic condition built with PXSelectBuilder or a PXFilterable attribute instead of string-building SQL by hand.
Hiding a button is not access control
A pattern I flag constantly: an action that's dangerous (voiding a payment, deleting a record, overriding an approval) is hidden from unauthorized users in the UI via a RowSelected visibility check, with no corresponding server-side check in the action's own implementation. Anyone who can construct the right REST call or reach the graph another way - a different screen sharing the same graph, an automation script - bypasses the hidden button entirely:
protected virtual IEnumerable voidPayment(PXAdapter adapter)
{
if (!PXAccess.HasPermission("AP.VoidPayment"))
throw new PXException("You do not have permission to void payments.");
// ... actual void logic
return adapter.Get();
}Validate at FieldVerifying or RowPersisting, not only client-side
Client-side validation (required-field markers, format masks) is UX, not security - it runs in the browser and is trivially bypassed by anyone calling the API directly. Any invariant that must actually hold (an amount that must be positive, a status transition that must follow a defined sequence) needs server-side enforcement in FieldVerifying or RowPersisting, which run regardless of entry point.
Don't let PXTrace or exception messages leak sensitive data
I've caught API keys, full credit card numbers, and customer PII in PXTrace.WriteInformation calls left in from debugging, shipped to production, and readable by anyone with access to the trace log. The same risk applies to exception messages surfaced to the UI - a caught exception from an external payment API that gets rethrown with its full response body embedded can leak more than intended to an end user's screen.
Treat anything written to PXTrace, the exception log, or a user-facing error message as something a support agent, another developer, or eventually a client's own staff will read. Log identifiers and outcomes, not raw payloads containing card numbers, tokens, or full API request/response bodies.
The checklist I actually run before sign-off
- Any raw SQL anywhere? If yes, is it fully parameterized - no string concatenation of user input, ever.
- Every dangerous action checked server-side with
PXAccess.HasPermission, not just hidden in the UI. - Every invariant that matters enforced in FieldVerifying/RowPersisting, not only client-side.
- No secrets, tokens, or full PII payloads in PXTrace calls or exception messages.
- File upload handlers (attachments, import screens) validate file type and size server-side, not by trusting the browser's reported MIME type.
Wrapping up
None of this is Acumatica-specific wisdom - it's ordinary web application security, applied to a framework that handles more of it automatically than most, which is exactly why the remaining gaps get overlooked. BQL, PXAccess, and server-side event validation cover the platform's part; the developer's part is not routing around them with string-concatenated SQL, UI-only permission checks, or verbose logging that leaks what it shouldn't.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.