From 2c45021cb37d6b60f33ebb945fa88e4ac71462b2 Mon Sep 17 00:00:00 2001 From: Djordje Cenic Date: Tue, 15 Sep 2026 13:16:53 +0200 Subject: [PATCH] Add security knowledge: validate unauthenticated endpoint responses New remedial article for spotting when AL calls an endpoint that does not authenticate itself to the client (bare HttpClient.Get, blank SOAP SecretText, post-DisableHttpsCheck HTTP) and requires the response to be size-, schema-, and request/response-integrity-validated before it is trusted. Includes the BC-specific false-positive clarifications (platform buffers the full body, so an in-AL size check after buffering is correct; no DNS-rebinding/bounded-read demand; HTTPS not always enforceable) plus good/bad AL samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...unauthenticated-response-before-use.bad.al | 23 ++++++++++ ...nauthenticated-response-before-use.good.al | 46 +++++++++++++++++++ ...ate-unauthenticated-response-before-use.md | 22 +++++++++ 3 files changed, 91 insertions(+) create mode 100644 microsoft/knowledge/security/validate-unauthenticated-response-before-use.bad.al create mode 100644 microsoft/knowledge/security/validate-unauthenticated-response-before-use.good.al create mode 100644 microsoft/knowledge/security/validate-unauthenticated-response-before-use.md diff --git a/microsoft/knowledge/security/validate-unauthenticated-response-before-use.bad.al b/microsoft/knowledge/security/validate-unauthenticated-response-before-use.bad.al new file mode 100644 index 0000000..ee0d25e --- /dev/null +++ b/microsoft/knowledge/security/validate-unauthenticated-response-before-use.bad.al @@ -0,0 +1,23 @@ +codeunit 50541 "Sec Sample UnauthResp Bad" +{ + procedure IsVatNumberValid(RequestedCountryCode: Text; RequestedVatNumber: Text): Boolean + var + HttpClient: HttpClient; + Response: HttpResponseMessage; + JsonResponse: JsonObject; + JsonToken: JsonToken; + Content: Text; + begin + // Anti-pattern: the endpoint is unauthenticated, yet the response is trusted with no + // size cap, no schema check, and no request-to-response integrity check. + HttpClient.Get('http://vat-service.example/check?cc=' + RequestedCountryCode + '&vat=' + RequestedVatNumber, Response); + Response.Content().ReadAs(Content); + JsonResponse.ReadFrom(Content); + + // Trusts valid=true for ANY input: a spoofed or MITM response that omits the echoed + // countryCode/vatNumber is accepted as valid for whatever number was requested. + if JsonResponse.Get('valid', JsonToken) then + exit(JsonToken.AsValue().AsBoolean()); + exit(false); + end; +} diff --git a/microsoft/knowledge/security/validate-unauthenticated-response-before-use.good.al b/microsoft/knowledge/security/validate-unauthenticated-response-before-use.good.al new file mode 100644 index 0000000..2c3e437 --- /dev/null +++ b/microsoft/knowledge/security/validate-unauthenticated-response-before-use.good.al @@ -0,0 +1,46 @@ +codeunit 50540 "Sec Sample UnauthResp Good" +{ + // The public VAT validation service does not authenticate itself to us (no OAuth, no + // certificate, plain HTTP), so its response must be validated before it is trusted. + procedure IsVatNumberValid(RequestedCountryCode: Text; RequestedVatNumber: Text): Boolean + var + HttpClient: HttpClient; + Response: HttpResponseMessage; + JsonResponse: JsonObject; + JsonToken: JsonToken; + Content: Text; + ResponseCountryCode: Text; + ResponseVatNumber: Text; + begin + HttpClient.Get('http://vat-service.example/check?cc=' + RequestedCountryCode + '&vat=' + RequestedVatNumber, Response); + if not Response.IsSuccessStatusCode() then + exit(false); + + Response.Content().ReadAs(Content); + + // 1) Size cap - the platform already buffered the whole body; reject abnormally large payloads. + if StrLen(Content) > 4096 then + Error('The VAT validation response exceeded the maximum allowed size and was rejected.'); + + // 2) Schema - require the expected scalar fields, not just a truthy flag. + if not JsonResponse.ReadFrom(Content) then + Error('The VAT validation response was not in the expected format and was rejected.'); + if not JsonResponse.Get('countryCode', JsonToken) then + Error('The VAT validation response did not include the requested identifiers and was rejected.'); + ResponseCountryCode := JsonToken.AsValue().AsText(); + if not JsonResponse.Get('vatNumber', JsonToken) then + Error('The VAT validation response did not include the requested identifiers and was rejected.'); + ResponseVatNumber := JsonToken.AsValue().AsText(); + + // 3) Integrity - the echoed identifiers must match the request, so a valid=true payload + // with the identifiers stripped cannot be accepted for an arbitrary VAT number. + if (UpperCase(ResponseCountryCode) <> UpperCase(RequestedCountryCode)) or + (UpperCase(ResponseVatNumber) <> UpperCase(RequestedVatNumber)) + then + Error('The VAT validation response did not match the requested identifiers and was rejected.'); + + if not JsonResponse.Get('valid', JsonToken) then + exit(false); + exit(JsonToken.AsValue().AsBoolean()); + end; +} diff --git a/microsoft/knowledge/security/validate-unauthenticated-response-before-use.md b/microsoft/knowledge/security/validate-unauthenticated-response-before-use.md new file mode 100644 index 0000000..2e99e2c --- /dev/null +++ b/microsoft/knowledge/security/validate-unauthenticated-response-before-use.md @@ -0,0 +1,22 @@ +--- +bc-version: [all] +domain: security +keywords: [unauthenticated, ssrf, httpclient, soap, response-validation, integrity, size-limit, disablehttpscheck, temp-blob, vies] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Validate responses from unauthenticated endpoints before trusting them + +## Description + +When AL calls an external endpoint that does not authenticate *itself* to the client, the response is fully attacker-influenceable — cleartext MITM, a spoofed or compromised host, DNS/redirect games, or simply a misbehaving public service. Recognizing that a call is unauthenticated is the first review step, and the signals are BC-specific: a bare `HttpClient.Get`/`Post` with no `Authorization` header, no acquired OAuth token, and no client certificate; a SOAP request whose credentials are blank, such as `SOAP Web Service Request Mgt.SetGlobals(..., '', BlankSecretText)`; or any request issued after `DisableHttpsCheck()` over plain HTTP (for example the EU VIES VAT service, whose default endpoint is `http://`). Because the BC platform HTTP stack buffers the entire response body before AL is handed the stream or `Temp Blob`, the whole payload is already in memory by the time AL parses it — so the response must be range- and shape-checked in AL *before* any of it is written to tax, VAT, customer, or vendor tables. + +## Best Practice + +Before parsing or trusting a response from an unauthenticated endpoint: (1) enforce a maximum size — reject when the buffered `Temp Blob` length or `Content-Length` exceeds a small cap sized to the expected payload; (2) validate the schema/shape — require the specific scalar nodes you expect, not merely "the body contains a truthy flag"; (3) enforce request-to-response integrity — when the protocol echoes the identifiers you queried (VIES echoes `countryCode`/`vatNumber`; a public-IP service echoes an IP string), require them to be present and to match the request, so a response carrying only `valid=true` cannot be accepted for an arbitrary input; (4) on rejection raise an `Error` and record a security audit via `Audit Log.LogAuditMessage(...)` plus telemetry. See sample: `validate-unauthenticated-response-before-use.good.al`. For validating the outbound target/host, see `validate-user-configurable-urls.md`; for authenticating outbound calls, see `prefer-oauth2-over-api-keys-for-external-http-calls.md`. + +## Anti Pattern + +Feeding the parsed response straight into business logic — load the XML/JSON, read a `valid` flag or an IP-shaped substring, then `Customer.Modify()` — trusting it purely because the HTTP call returned 2xx, with no size, shape, or echoed-identifier check. Reviewers should flag an unauthenticated outbound call (no `Authorization`/OAuth/cert, blank SOAP `SecretText`, or a request after `DisableHttpsCheck`) whose response is parsed and persisted without a preceding size cap, schema check, and request-to-response integrity check. Do NOT, however, demand a streaming or bounded read that aborts the transfer mid-download, nor a resolved-IP/DNS-rebinding check: the platform buffers the full body before AL sees it and AL has no connection-time or DNS hook, so an in-AL size check necessarily runs after buffering and host-rebinding defense belongs to the platform egress layer — raising those is a false positive. HTTPS is likewise not always enforceable (VIES is HTTP by design); the mitigation there is response validation, not scheme enforcement. See sample: `validate-unauthenticated-response-before-use.bad.al`.