Skip to content

Latest commit

 

History

History
149 lines (120 loc) · 10.1 KB

File metadata and controls

149 lines (120 loc) · 10.1 KB

Security

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.

Supported versions

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.

Reporting a vulnerability

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.

Automated checks in CI

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.

Security-relevant package behaviour

Each of these is a decision the packages make on your behalf. Read the ones you use.

Secrets and randomness

  • Use DKNet.RandomCreator for anything secret. It wraps System.Security.Cryptography.RandomNumberGenerator; System.Random is not cryptographically secure and its output is predictable. See DKNet.RandomCreator.

Encryption keys

  • AddEncryptionServices() registers no cipher. It registers IShaHashing and IHmacHashing only. AES and RSA are opt-in through AddAesGcmEncryption(base64Key) and AddRsaEncryption(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.Encryption fails closed. It needs an IEncryptionKeyProvider registered through AddEfCoreEncryption<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. Use IAesGcmEncryption, which authenticates the ciphertext and uses a fresh random nonce per call. See DKNet.Svc.Encryption.

Row-level isolation

  • AddDataOwnerProvider<TDbContext, TProvider>() requires IDataOwnerDbContext on 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 AccessibleKeys denies access; it is never read as "see everything". Unrestricted access is an explicit opt-in via IsUnrestrictedAccess, which defaults to false.
  • The filter is registered process-wide. AddDataOwnerProvider adds it to a static model-builder list, so every DbContext that calls UseAutoConfigModel() applies it. A second context holding IOwnedBy entities must also implement IDataOwnerDbContext, or keep those entities out of its model.
  • The ownership key is not an identity. OwnedBy says which tenant a row belongs to, never who touched it. Register an ICurrentUserProvider (see Audit trails below) when CreatedBy/UpdatedBy must 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.

Audit trails

  • 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 any SecureString property 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.Encryption is redacted in audit entries. [Encrypted] uses a ColumnEncryptionConverter, a ValueConverter — so the change tracker holds the decrypted model value, and without this redaction that plaintext would reach every registered IAuditLogPublisher. DKNet.EfCore.AuditLogs treats [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 ICurrentUserProvider via AddCurrentUserProvider<TDbContext, TProvider>() fills CreatedBy/UpdatedBy from ICurrentUserProvider.GetCurrentUser() instead of from the tenant ownership key. Whatever that method returns reaches every registered IAuditLogPublisher in 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.DataAuthorization fills CreatedBy/UpdatedBy from the ownership key, so the trail silently records a tenant instead of a person. Check which one your reports assume.

File storage

  • DKNet.Svc.BlobStorage.Local rejects path traversal. A resolved path that escapes the configured RootFolder raises UnauthorizedAccessException rather than reading or writing outside it.
  • All three adapters share the same validation. BlobService.ValidateFile checks MaxFileNameLength, IncludedExtensions, and MaxFileSizeInMb and throws FileLoadException on a violation; Azure, S3, and local all call it at the top of their SaveAsync, so an extension allow-list applies uniformly. It is protected virtual, so a custom adapter of your own must call it — the base class does not force it. See DKNet.Svc.BlobStorage.Abstractions.

Idempotency keys

  • A client-supplied key is validated before use. DKNet.AspCore.Idempotency checks 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 Authorization header (only when IdempotencyOptions.ScopeHmacSecret is configured — otherwise that fallback is skipped), then the client IP if IncludeClientIpInScope is set. Neither the secret nor the raw header is ever logged. A KeyScopeResolver replaces 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 unless AddIdempotentKey/AddIdempotencyWith*Store registered an IIdempotencyKeyStore — the route then fails on its first request. Between two named stores AddIdempotentKey is first-registration-wins: a second call with different options is silently ignored, so a stricter Expiration or a ScopeHmacSecret added 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.

Your responsibilities

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.