knowledge(security): a guarded Text credential seam is legitimate when SecretText cannot flow end to end

This commit is contained in:
Nabil BA-MOH 2026-08-27 22:06:11 +08:00
parent 293f9b13e7
commit 275e1a6be5
3 changed files with 76 additions and 0 deletions

View file

@ -0,0 +1,18 @@
interface "Sec Sample Transport Bad"
{
procedure Send(Uri: Text; AuthorizationHeader: Text): Boolean;
}
codeunit 50541 "Sec Sample Text Seam Bad"
{
var
Transport: Interface "Sec Sample Transport Bad";
// Half conversion: the parameter became SecretText to satisfy the analyzer,
// but the seam is still Text, so the value is unwrapped right away.
// Unwrap is on-premises only: this does not hold for a cloud-targeted app.
procedure Call(Uri: Text; ApiKey: SecretText): Boolean
begin
exit(Transport.Send(Uri, 'Bearer ' + ApiKey.Unwrap()));
end;
}

View file

@ -0,0 +1,30 @@
interface "Sec Sample Transport Good"
{
// The seam is Text by design: the test double asserts WHERE the credential travels.
procedure Send(Uri: Text; AuthorizationHeader: Text): Boolean;
}
codeunit 50540 "Sec Sample Text Seam Good"
{
var
Transport: Interface "Sec Sample Transport Good";
[NonDebuggable]
procedure Call(Uri: Text; ServiceCode: Code[20]): Boolean
var
ApiKey: Text;
begin
// Plain-text path kept as short as possible; value is encrypted at rest.
#pragma warning disable LC0043 // Text seam: the transport interface and its test double are typed Text on purpose
if not IsolatedStorage.Get(ServiceCode, DataScope::Company, ApiKey) then
exit(false);
exit(Transport.Send(Uri, 'Bearer ' + ApiKey));
#pragma warning restore LC0043
end;
[NonDebuggable]
procedure Store(ServiceCode: Code[20]; ApiKey: Text)
begin
IsolatedStorage.SetEncrypted(ServiceCode, ApiKey, DataScope::Company);
end;
}

View file

@ -0,0 +1,28 @@
---
bc-version: [23..]
domain: security
keywords: [secrettext, text-seam, interface, nondebuggable, unwrap, lc0043, pragma, half-conversion, test-double]
technologies: [al]
countries: [w1]
application-area: [all]
---
# A guarded Text credential seam is legitimate when SecretText cannot flow end to end
> Contributions welcome — open a PR to refine or extend this article.
## Description
`SecretText` can only protect a credential along a path where every API accepts it. Some seams are typed `Text` by design: an injectable interface whose test double asserts *where* the credential travels (header versus body), or a legacy consumer that takes `Text`. Retyping such a seam to `SecretText` does not complete the conversion — the value must become `Text` again at the seam, and `SecretText.Unwrap()` is on-premises only (see `nondebuggable-required-when-unwrapping-secrettext.md`). A cloud app therefore cannot convert this path end to end, and a partial conversion is worse than none: it either does not build for the cloud target or destroys the test that pinned the credential's placement. An analyzer such as LinterCop LC0043 flags the `Text` seam; the rule is right in general and wrong at exactly this location.
## Best Practice
When a `Text` seam cannot be removed, keep it `Text` and guard it instead: mark every procedure that touches the plain value `[NonDebuggable]`, keep the plain-text path as short as possible, store the value encrypted at rest (`IsolatedStorage` with `SetEncrypted`), and suppress the analyzer with a `#pragma` scoped to the exact statements, restored immediately, with a comment naming the seam that justifies it. Prefer removing the seam when the callee has a `SecretText`-aware API (`HttpHeaders.Add`, `HttpContent.WriteFrom`, `SetSecretRequestUri` — see `secrettext-with-httpclient.md`); the guarded `Text` seam is the fallback, not the default.
See sample: `guarded-text-credential-seam-is-legitimate-when-secrettext-cannot-flow-end-to-end.good.al`.
## Anti Pattern
A review finding that demands `SecretText` on a guarded `Text` seam without checking whether the value can stay `SecretText` up to the sink. Acting on it produces the half conversion: a `SecretText` parameter that is immediately `Unwrap()`-ed to call the `Text` seam, or a seam retyped to `SecretText` whose test double can no longer assert the credential's placement. Signals to detect the half conversion: `Unwrap()` in a cloud-targeted app, or a `SecretText` parameter converted back to `Text` within the same procedure.
See sample: `guarded-text-credential-seam-is-legitimate-when-secrettext-cannot-flow-end-to-end.bad.al`.