A legacy application succeeds because some of its code can do more than the person using it. During migration, that behavior is easy to lose and equally easy to preserve too broadly. Replacing a permission error with elevated execution can make the demonstration work while removing the application's security boundary.
The decision is not whether every farm solution must become SPFx. It is which behavior needs farm-local execution, which caller may request it, and which identity is permitted to carry it out on each resource.
This analysis addresses SharePoint Server 2016 and 2019 customizations moving toward Subscription Edition. The examples are original and hypothetical. Microsoft Learn was reviewed on 1 November 2026. Historical server object-model guidance is identified as historical; it is not presented as a fresh compatibility test against SE. No farm or tenant was accessed.
Separate three meanings of server-side
Microsoft distinguishes farm solutions, which call the SharePoint server object model on SharePoint servers, from remotely hosted code, which calls client interfaces. Its farm-solution guidance links older SDK material and explicitly warns that the historical terminology needs that distinction. Build farm solutions
A .NET web API outside the farm is server-side code in ordinary architecture language. It is not thereby SharePoint server object-model code. SPFx runs client-side and cannot be made an authority for privileged work by hiding a button.
Record the execution host before choosing a migration tactic: web request, timer job, event receiver, feature receiver, command-line administration or remote service. The same helper method called from two hosts may inherit different identities, lifetimes and failure behavior. A single successful browser test will not cover those paths.
What elevation changes, and what it must not decide
SPSecurity.RunWithElevatedPrivileges belongs to the farm-side object model. Microsoft's elevation of privilege guidance describes a delegate that runs "in the context of an account with higher privileges than the current user", a new SPSite opened inside it when changes are made, and validation of the form digest before an elevated write. It adds that elevation "should be used with care. You should not expose direct, uncontrolled mechanisms for people with low privileges to circumvent the permissions granted to them."
That mechanism answers how code can execute with a different authority. It does not answer whether the original caller was entitled to request the business action. The application must make that decision before using the stronger authority.
The distinction is visible in a hypothetical approval service. A reader can view a request. An approver can approve a request within an assigned scope. The implementation identity may need rights the reader does not have. Granting those implementation rights is not a reason to accept approval requests from every reader.
The historical developer security guidance requires explicit permission checks where elevation bypasses the normal checks, because elevated operations "always succeed regardless of the user's permissions", and warns against letting elevated object-model objects escape their scope. It also describes the block as running "by using service account credentials", with SharePoint objects opened inside it attributed to the System Account. That guidance was written for SharePoint 2010: a reason to confirm the behaviour on the target build, not to ignore it. Security best practices, SharePoint 2010
Review the boundary, not just the method name
For each elevated call, reconstruct this record from the actual code and configuration:
| Question | Evidence to collect | Failure that a successful deployment misses |
|---|---|---|
| Who initiated the operation? | Original caller identity and entry point | A background identity is mistaken for the human authorizing the action |
| What may that caller request? | Business authorization on the intended resource | A low-privilege caller chooses a different target identifier |
| Where are objects created? | Lifetime of SPSite, SPWeb and dependent objects | Reusing request-context objects gives different behavior from newly opened objects |
| What leaves the elevated scope? | Return types and references retained by callbacks | An elevated object remains usable by later code |
| What can the execution identity reach? | Farm, filesystem, database and API permissions examined separately | An unrelated external resource becomes reachable |
| What happens twice? | Retry, event re-entry and idempotency behavior | A timeout leads to duplicate business effects |
| What is recorded? | Caller, operation, target, execution identity and result, with sensitive values minimized | Logs show only a system identity and cannot explain the request |
| How is the elevated site opened? | The URL or zone passed to the SPSite constructor | A site opened by ID and user token alone, outside the Default zone, can bypass web application policy |
Replacing the elevated call with a service account, or with an add-in's app-only permission, changes which identity acts; it answers none of the questions in the table. Microsoft's add-in guidance notes that the service account's name is then what the Modified By column shows, and that app-only policy acts with the add-in's own permissions rather than on behalf of a user. Each alternative is designed with the same record, or the requester disappears from it.
Do not dispose objects owned by the request context as if your function created them. Conversely, do not leave newly created disposable objects without an owner. Do not return a live SharePoint object merely to avoid mapping a small result. Those are code-review obligations to confirm against the applicable object-model guidance and the target runtime, not a claim that this article ran that runtime.
AllowUnsafeUpdates is not a replacement for caller authorization or a general CSRF solution. If it appears in the old implementation, establish why the code requires it, the request context in which it runs and how the previous value is restored on failure. Do not mechanically carry a bypass into the replacement service. Microsoft documents form-digest validation as protection against unwanted submitted operations, a different concern from permission to perform the requested business action. Form digest protocol purpose
Test authorization without pretending to test SharePoint
This small C# program exercises an invented business boundary. It uses no SharePoint assembly, credentials or network. The counterexample is the reader who asks for the same privileged operation as the approver.
static bool MayRequest(string role, string action, string target) =>
role == "Approver" && action == "Approve" && target == "request-a";
var executions = 0;
foreach (var role in new[] { "Reader", "Approver" })
{
var allowed = MayRequest(role, "Approve", "request-a");
if (allowed) executions++;
Console.WriteLine($"{role}: {allowed}");
}
if (MayRequest("Approver", "Approve", "request-b"))
throw new Exception("Target boundary was lost");
Console.WriteLine($"Privileged operations requested: {executions}");The observed local output is:
Reader: False
Approver: True
Privileged operations requested: 1This establishes only that the illustrative function enforces its stated role/action/target condition. Real authorization cannot trust a role string supplied by a browser. Identity must come from the authenticated request and policy must use authoritative application data. Race conditions also matter: a request may change state between a permission check and an update. The replacement needs a concurrency policy; copying this function into production would not provide one.
Keep, adapt, replace or eliminate
Keep a bounded farm component when it genuinely requires supported farm-local behavior and the team can maintain and test it. Rebuild against the target dependencies, inspect removed features and validate the operation on the intended SE build. Installation success proves neither complete support nor secure behavior.
Adapt it within the farm when the current component mixes business policy, SharePoint access and privilege changes. Separate the policy decision from the privileged adapter. This can make a later extraction cheaper without forcing the whole application to cross a network boundary during the platform upgrade.
Replace it with a remote service when the business operation can use supported remote APIs and a separately managed identity. This can reduce farm coupling, but introduces API authorization, network availability, credential or certificate lifecycle, retries and observability. A modern .NET runtime in that service is not permission to transplant the farm object model into it. A browser token accepted by the service is not automatically a token accepted by SharePoint.
Eliminate when nothing that matters depends on the operation any more. The cheapest elevated operation to secure is the one that no longer runs; usage evidence from the inventory decides it, not the age of the code.
These options are recommendations to evaluate, not a declaration that any particular legacy assembly is compatible. SE upgrade guidance requires custom components to be prepared before content is attached; Test-SPContentDatabase helps identify missing components. It does not execute every business path in an assembly. Upgrade content databases
Keep product support boundaries intact
An Online retirement does not settle a Server decision. Microsoft's Add-in retirement notice explicitly distinguishes SharePoint Online from Server Add-ins deployed through an on-premises app catalog. It also distinguishes Add-ins from REST, CSOM, JSOM and SPFx. That exception is not a blanket approval of every authentication dependency used by an old application. Add-in retirement scope
Likewise, the SPFx compatibility reference lists different capabilities for Online, SE, 2019 and 2016 with Feature Pack 2. Use the row for the actual platform and check the applicable release notes. A current Online toolchain is not a migration destination for every server-side customization. SPFx platform compatibility
Acceptance evidence before business use
Rehearse authorized and denied requests, altered target identifiers, expired identity material, unavailable dependencies and retries after uncertain completion. Run the web, timer and event paths that actually exist. Compare the test and production dependency configurations; record differences instead of treating a similarly named environment as equivalent.
The result should name the tested assembly, package, configuration and farm build. It should include functional acceptance as well as technical diagnostics. Where that evidence is absent, say the operation has not yet been validated on SE. Do not convert a package deployment into a statement that the business application is ready.
Field Guide chapter 17 turns these questions into a procedure: finding each elevated operation, the account it acts as, and the tests that prove it on a rehearsal farm. The Subscription Edition migration pack generates an elevated operations inventory and an assessment built on these questions, with the customisation inventory and the database-attach runbook around them. The existing Server development article discusses the client-side toolchain. This is the complementary server-side decision: preserve the required business authority while reducing accidental privilege. The desired outcome is not simply fewer elevation calls. It is an application whose authorized behavior remains explainable after the migration.