This page covers what DKNet supports with security fixes, how to report a vulnerability, and the
security-relevant behaviour of the packages themselves. The repository root
SECURITY.md is the short version and links here.
DKNet has no long-term-support branches. Packages are published from main by
.github/workflows/dotnet-publish.yml, which derives each version from the commit history with
paulhatch/semantic-version in major.minor.patch form, so
there is no maintained matrix of older lines.
| Version | Supported |
|---|---|
| The latest version of a package on NuGet | ✅ Fixes land here |
| Any earlier version | ❌ Upgrade to the latest |
If upgrading is blocked by a breaking change, the Migration Guide documents the ones that have shipped.
Do not open a public issue for a security problem.
Use GitHub's private vulnerability reporting for this repository: https://github.com/baoduy/DKNet/security/advisories/new. That channel is private to the maintainers until an advisory is published.
Please include the affected package and version, what an attacker can do, and a minimal reproduction. There is no published response-time commitment — DKNet is a volunteer-maintained open-source project.
Two scanners run over the repository, and their findings are triaged like any other defect:
- CodeQL —
.github/workflows/codeql.yml, C# analysis on push and pull request. - Qodana — configured by
qodana.yaml.
src/Directory.Build.props sets TreatWarningsAsErrors and Nullable=enable solution-wide, so a nullability
mistake in a security-relevant path fails the build rather than shipping.
Each of these is a decision the packages make on your behalf. Read the ones you use.
- Use
DKNet.RandomCreatorfor anything secret. It wrapsSystem.Security.Cryptography.RandomNumberGenerator;System.Randomis not cryptographically secure and its output is predictable. See DKNet.RandomCreator.
AddEncryptionServices()registers no cipher. It registersIShaHashingandIHmacHashingonly. AES and RSA are opt-in throughAddAesGcmEncryption(base64Key)andAddRsaEncryption(privateKeyBase64), so the key is something you supply and persist. An earlier release registered AES over a randomly generated key that was never persisted — ciphertext produced under that registration cannot be recovered. Details: Migration Guide.DKNet.EfCore.Encryptionfails closed. It needs anIEncryptionKeyProviderregistered throughAddEfCoreEncryption<TKeyProvider>(); without usable key material the model build fails rather than storing plaintext. See DKNet.EfCore.Encryption.IAesEncryption(AES-CBC) has been removed, not just deprecated. It used a fixed IV embedded in the key, so identical plaintexts always produced identical ciphertext — a real information leak, not a theoretical one. UseIAesGcmEncryption, which authenticates the ciphertext and uses a fresh random nonce per call. See DKNet.Svc.Encryption.
AddDataOwnerProvider<TDbContext, TProvider>()requiresIDataOwnerDbContexton the context. The constraint exists because the older, unconstrained signature allowed a context the ownership filter could not read — which silently disabled row isolation.- An empty
AccessibleKeysdenies access; it is never read as "see everything". Unrestricted access is an explicit opt-in viaIsUnrestrictedAccess, which defaults tofalse. - The filter is registered process-wide.
AddDataOwnerProvideradds it to a static model-builder list, so everyDbContextthat callsUseAutoConfigModel()applies it. A second context holdingIOwnedByentities must also implementIDataOwnerDbContext, or keep those entities out of its model. - The ownership key is not an identity.
OwnedBysays which tenant a row belongs to, never who touched it. Register anICurrentUserProvider(see Audit trails below) whenCreatedBy/UpdatedBymust answer the second question; without one they are filled from the ownership key, which answers only the first. - Dropping
UseAutoConfigModel<TContext>()removes the filter. This matters most in tests: without it a query returns rows it never would in production. See DKNet.EfCore.DataAuthorization.
- Sensitive-looking values are redacted by default. Under the default
AuditPropertyPolicy.RedactSensitive, properties whose name matches a built-in deny-list (password,secret,token,apikey,ssn,creditcard,connectionstring,privatekey, …) and anySecureStringproperty are captured as"***REDACTED***"— the field still shows that it changed, never its value. [AuditLog]on a property forces plaintext capture. Do not put it on a secret.[SensitiveData]always redacts and cannot be overridden by[AuditLog];[IgnoreAuditLog]removes the property from the trail entirely. See DKNet.EfCore.AuditLogs.- A column encrypted via
DKNet.EfCore.Encryptionis redacted in audit entries.[Encrypted]uses aColumnEncryptionConverter, aValueConverter— so the change tracker holds the decrypted model value, and without this redaction that plaintext would reach every registeredIAuditLogPublisher.DKNet.EfCore.AuditLogstreats[Encrypted]as unconditionally sensitive, the same as[SensitiveData]. See DKNet.EfCore.Encryption. - The audit identity can be a person, and it is published unmasked. Registering an
ICurrentUserProviderviaAddCurrentUserProvider<TDbContext, TProvider>()fillsCreatedBy/UpdatedByfromICurrentUserProvider.GetCurrentUser()instead of from the tenant ownership key. Whatever that method returns reaches every registeredIAuditLogPublisherin full — the redaction rules above cover entity property values, not the audit identity. An application subject to a personal-data rule (GDPR, PDPA) must therefore return a stable, non-personal identifier such as the token subject id, not an email address or any other directly identifying value. - Dropping the provider does not blank the audit fields — it changes what they mean. With no current-user
value for a save,
DKNet.EfCore.DataAuthorizationfillsCreatedBy/UpdatedByfrom the ownership key, so the trail silently records a tenant instead of a person. Check which one your reports assume.
DKNet.Svc.BlobStorage.Localrejects path traversal. A resolved path that escapes the configuredRootFolderraisesUnauthorizedAccessExceptionrather than reading or writing outside it.- All three adapters share the same validation.
BlobService.ValidateFilechecksMaxFileNameLength,IncludedExtensions, andMaxFileSizeInMband throwsFileLoadExceptionon a violation; Azure, S3, and local all call it at the top of theirSaveAsync, so an extension allow-list applies uniformly. It isprotected virtual, so a custom adapter of your own must call it — the base class does not force it. See DKNet.Svc.BlobStorage.Abstractions.
- A client-supplied key is validated before use.
DKNet.AspCore.Idempotencychecks the header's presence, format, and length before it reaches a store. - Keys are scoped per caller, so one client cannot replay another's response. The composite key includes a
caller scope resolved from the authenticated user, then an HMAC-SHA256 digest of the
Authorizationheader (only whenIdempotencyOptions.ScopeHmacSecretis configured — otherwise that fallback is skipped), then the client IP ifIncludeClientIpInScopeis set. Neither the secret nor the raw header is ever logged. AKeyScopeResolverreplaces the whole chain. See DKNet.AspCore.Idempotency. - Register the store, and register it once.
.RequiredIdempotentKey()always adds the filter, but the filter cannot be constructed unlessAddIdempotentKey/AddIdempotencyWith*Storeregistered anIIdempotencyKeyStore— the route then fails on its first request. Between two named storesAddIdempotentKeyis first-registration-wins: a second call with different options is silently ignored, so a stricterExpirationor aScopeHmacSecretadded later may never take effect. Cover the registration with a start-up test. - The parameterless
AddIdempotentKey()is a local-development default, not a deployment one. Its in-process store reserves each key atomically, so it is not a concurrency hazard — but the keys live in one process's memory: they are lost on restart and not shared between instances. A multi-instance deployment left on it keeps one ledger per instance and therefore loses cross-instance idempotency, letting the same key be processed once per instance. That is an operational failure rather than an attack surface — the store holds only process-own memory, opens no listener, takes no credential, and adds no configuration input. The mitigation is the startup warning the app logs while that store is the one serving requests: treat it as a deployment defect and switch to the SQL Server, PostgreSQL, or Redis store, which reserve the key atomically across instances.
DKNet does not authenticate or authorise callers. It gives you row-level ownership filtering, column encryption, audit redaction, and idempotency; authentication, authorisation policies, transport security, and key rotation remain yours. Keep the values listed in Environment and secrets out of source control.