18 KiB
Security Audit Report — GoodGo POS System
Auditor: Security Engineer Date: 2026-03-20 Scope: Auth implementation, OWASP risks, secrets management, CORS, dependencies Classification: INTERNAL — CONFIDENTIAL
Executive Summary
The GoodGo POS System demonstrates a solid security architecture in its design patterns (CQRS, Clean Architecture, MediatR pipeline with validation, account lockout, TOTP-based 2FA, comprehensive audit logging). However, there is one systemic critical failure that overrides all other positives: real production credentials are hardcoded and tracked in git across 19+ microservices. Until this is remediated, the entire platform is at risk of full compromise. Additionally, JWT tokens are stored in localStorage (XSS-extractable), CSP headers are absent, and the MCP server has a live bearer token committed to version control.
Critical Issues
CRIT-01 — Production Database Credentials Committed to Git (19 Services)
Severity: Critical
CVSS: 9.8 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
Affected Components: All 19 .NET microservices appsettings.json
Description: The Neon PostgreSQL production password npg_Ssfy6HKO0cXI and the Redis production password Velik@2026 (with public IP 167.114.174.113) are hardcoded in appsettings.json files that are tracked by git.
Affected Files (sample — all 19 services affected):
services/iam-service-net/src/IamService.API/appsettings.json:33
services/order-service-net/src/OrderService.API/appsettings.json:33
services/wallet-service-net/src/WalletService.API/appsettings.json:33
services/merchant-service-net/src/MerchantService.API/appsettings.json:33
services/chat-service-net/src/ChatService.API/appsettings.json:33
... (14 more services)
Leaked Credentials:
PostgreSQL: Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech
Username=neondb_owner | Password=npg_Ssfy6HKO0cXI
Redis: Host=167.114.174.113:6379 (public IP)
Password=Velik@2026
SMTP: SmtpPassword=a469e9333580ef5dbb141f01e33864ef-51afd2db-6c014754
(appsettings.json:54 in iam-service-net)
JWT Secret: goodgo-iam-service-secret-key-32chars!
(appsettings.json:44 in iam-service-net)
Impact: Anyone with read access to this repository can authenticate directly to the production database and Redis. All customer data, merchant data, orders, wallets, and PII are at risk of exfiltration or destruction.
Proof of Concept:
psql "Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;Database=iam_service;SSL Mode=Require"
Remediation:
- Rotate all credentials immediately — DB password, Redis password, SMTP key, JWT secret
- Replace all
appsettings.jsonvalues with environment variable placeholders:"DefaultConnection": "" - Inject secrets at runtime via Kubernetes Secrets or HashiCorp Vault
- Add
appsettings.*.jsonpattern to.gitignorefor non-Development configs, or use User Secrets locally - Run
git filter-repoor BFG Repo Cleaner to purge secrets from git history
Timeline: IMMEDIATE — within 24 hours
CRIT-02 — Active JWT Bearer Token Committed in MCP Server .env
Severity: Critical
Affected Component: services/goodgo-mcp-server/.env:3
Description: A live, signed JWT bearer token is committed to git history in the MCP server's .env file, which is tracked by git (git ls-files confirms).
API_TOKEN=eyJhbGciOiJSUzI1NiIsImtpZCI6IjQ3NjE1OTQ3...
Decoded payload includes: sub, email, auth_time, jti — a real user/service account token.
Impact: Any party with repository access can replay this token to authenticate as the associated service account until it expires or is revoked. The MCP server has 12 operational tools that can read/write F&B data.
Remediation:
- Revoke the token immediately via IAM service
- Remove
.envfrom git tracking: addservices/goodgo-mcp-server/.envto.gitignore - Purge from git history
- Rotate the service account credentials
Timeline: IMMEDIATE — within 24 hours
CRIT-03 — IdentityServer Using AddDeveloperSigningCredential() in All Environments
Severity: Critical
Affected Component: services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs:142
Description: The code unconditionally calls AddDeveloperSigningCredential(), which generates a temporary RSA key on startup that is not persisted between restarts. This means:
- JWT tokens are invalidated on every service restart
- There is no certificate-based signing in production
- The signing key changes silently
.AddDeveloperSigningCredential(); // Line 142 — applied in ALL environments
Impact: In production, this causes all active sessions to be invalidated on restart. More critically, a production RSA private key should be stored in a secret vault (Vault/K8s Secret), not re-generated on each boot.
Remediation:
if (env.IsDevelopment())
identityServer.AddDeveloperSigningCredential();
else
identityServer.AddSigningCredential(LoadCertificateFromVault()); // HSM or K8s TLS secret
Timeline: Before next production deployment
Warnings
WARN-01 — JWT Tokens Stored in localStorage (XSS Risk)
Severity: High
Affected Component: apps/web-client-tpos-net/src/WebClientTpos.Client/Services/AuthService.cs:147
Description: Access tokens are stored in localStorage using role-specific keys (aPOS_token_owner, aPOS_token_staff):
await _js.InvokeVoidAsync("localStorage.setItem", TokenKey(role), token.AccessToken);
localStorage is accessible by any JavaScript running on the same origin. If a single XSS vulnerability is introduced anywhere in the Blazor WASM app, an attacker can exfiltrate all tokens.
Impact: XSS → token theft → full account takeover.
Remediation:
- Use
HttpOnlycookies for token storage (tokens never accessible to JS) - If
localStorageis retained for Blazor WASM constraints, implement strict CSP (see WARN-02) to minimize XSS risk - Add token binding or short expiry (already 15 min — good) with refresh rotation
WARN-02 — No Content-Security-Policy (CSP) Header
Severity: High
Affected Component: infra/traefik/dynamic/middlewares.yml
Description: The secure-headers middleware is missing Content-Security-Policy:
secure-headers:
headers:
contentTypeNosniff: true # ✓
browserXssFilter: true # ✓ (deprecated but present)
frameDeny: true # ✓
stsSeconds: 31536000 # ✓
# CSP: MISSING ✗
# Referrer-Policy: MISSING ✗
# Permissions-Policy: MISSING ✗
Without CSP, the browser has no instructions to block inline scripts, restrict eval(), or limit allowed resource origins — the primary defense against XSS exploitation.
Remediation — Add to secure-headers middleware:
contentSecurityPolicy: >
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.goodgo.vn wss://;
font-src 'self';
object-src 'none';
frame-ancestors 'none';
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
Note: Blazor WASM requires wasm-unsafe-eval for .NET runtime compilation.
WARN-03 — CORS allowCredentials: true with Multiple Origins
Severity: High
Affected Component: infra/traefik/dynamic/middlewares.yml:27
Description:
cors:
headers:
accessControlAllowOriginList:
- "http://localhost:3000" # Development origins
- "http://localhost:3001"
- "https://goodgo.vn" # Production origin
accessControlAllowCredentials: true # ← HIGH RISK
Combining allowCredentials: true with a list of origins that includes localhost development URLs is dangerous. If a developer's machine is compromised, the credentials flag allows the attacker to make authenticated cross-origin requests from any of these origins.
Remediation:
- Per-environment CORS config: production allows only
https://goodgo.vn - Development config in a separate file that is NOT applied to production
admin.goodgo.vnshould be added to production allowlist if used
WARN-04 — sslRedirect: false in Shared Middleware Config
Severity: High
Affected Component: infra/traefik/dynamic/middlewares.yml:5
Description:
sslRedirect: false # EN: Disabled for local development
This file is used across all environments. If this same middlewares.yml is deployed to staging/production (common in this monorepo pattern), HTTPS redirection will be disabled.
Remediation:
- Separate
middlewares.ymlper environment, or use Traefik environment variable substitution - Explicitly set
sslRedirect: truein staging/production overlay
WARN-05 — Jwt__RequireHttpsMetadata=false Across All Services
Severity: High
Affected Component: deployments/local/docker-compose.yml (lines ~219, 265, 372, 420, etc.)
Description: Every service in docker-compose overrides JWT metadata HTTPS requirement:
- Jwt__RequireHttpsMetadata=false
This is acceptable for local Docker networking. However, if these environment variables are inadvertently propagated to staging/production K8s ConfigMaps, JWT validation will succeed over plain HTTP.
Remediation:
- Verify K8s ConfigMaps do NOT contain this override:
grep -r RequireHttpsMetadata deployments/staging/ deployments/production/ - Add to deployment runbook:
RequireHttpsMetadatamust betruein staging and production
WARN-06 — Traefik Dashboard Exposed Without Authentication
Severity: Medium
Affected Component: deployments/local/docker-compose.yml:121
Description:
command:
- "--api.insecure=true"
Traefik API/dashboard is accessible at http://localhost:8080 with no authentication in local development. While local-only, this exposes full routing configuration, middleware definitions, and service topology.
Remediation:
- Use
--api.insecure=falseand enableapi.dashboard: truewithBasicAuthmiddleware - Verify staging/production Traefik deployment does NOT have
--api.insecure=true
WARN-07 — Unauthenticated Ad Tracking Endpoints Without Rate Limiting
Severity: Medium
Affected Component: Routes for /api/v1/pixels, /api/v1/conversions, /api/v1/ads/events
Description: These endpoints are intentionally public (pixel tracking requires no auth) but have no documented rate limiting in the Traefik route labels. The only defined rate limits are auth-ratelimit, payment-ratelimit, api-ratelimit, and hub-ratelimit.
Impact: An attacker can spam fake conversion/pixel events to corrupt analytics, inflate ad billing, or DoS the tracking services.
Remediation:
- Apply a dedicated
tracking-ratelimit(e.g., 200 req/min per IP) to these routes - Implement HMAC signature validation on pixel requests with a short-lived nonce
WARN-08 — AllowedHosts Wildcard in IAM Service
Severity: Medium
Affected Component: services/iam-service-net/src/IamService.API/appsettings.json:79
Description:
"AllowedHosts": "*"
A wildcard AllowedHosts allows the IAM service (which issues JWT tokens) to respond to requests regardless of the Host header value. This can be exploited in DNS rebinding attacks or Host header injection to redirect token issuance.
Remediation:
"AllowedHosts": "iam-service;localhost;127.0.0.1"
Production should explicitly list iam.goodgo.vn or the internal K8s service name.
WARN-09 — K8s Staging Secrets File Contains Placeholders
Severity: Medium
Affected Component: deployments/staging/kubernetes/secrets.yaml
Description: The secrets manifest contains literal placeholder strings:
Jwt__Secret: "PLACEHOLDER-staging-jwt-secret-min-32-chars"
If a CI/CD pipeline or developer applies this file directly without substitution, the platform will launch with predictable secrets.
Remediation:
- Use
secrets.yaml.examplepattern (like production) — remove the live stagingsecrets.yamlfrom git - Adopt External Secrets Operator or Sealed Secrets for K8s secret management
- Add a CI pre-flight check that rejects
PLACEHOLDERvalues in K8s secret manifests
WARN-10 — TOTP Verification Window Allows 90-Second Replay
Severity: Low
Affected Component: services/iam-service-net/src/IamService.Infrastructure/TwoFactor/TotpTwoFactorService.cs:86
Description:
new VerificationWindow(previous: 1, future: 1)
A tolerance of ±1 time step (30 seconds each) means a valid TOTP code has a 90-second effective validity window. Combined with no used-code tracking, a captured OTP could be replayed within 90 seconds.
Remediation:
- Track used TOTP codes in Redis with a 90-second TTL to prevent replay
- Consider reducing window to
previous: 0, future: 0if clock skew is controlled
Improvements
IMP-01 — Implement Secrets Management Pipeline
Replace all hardcoded credentials with a proper secrets lifecycle:
Local Dev: dotnet user-secrets / .env (gitignored)
CI/CD: GitHub Actions Secrets → injected at build time
Staging/Prod: Kubernetes External Secrets Operator → HashiCorp Vault
or AWS Secrets Manager / Azure Key Vault
Reference implementation pattern for each service:
// Program.cs — replace appsettings.json secret loading
builder.Configuration.AddEnvironmentVariables();
// In K8s: environment variables injected from K8s Secrets
IMP-02 — Add Serilog Sensitive Data Destructuring
Prevent accidental PII/credential logging:
Log.Logger = new LoggerConfiguration()
.Destructure.ByTransforming<ApplicationUser>(u => new { u.Id, u.Email })
.Filter.ByExcluding(e => e.Properties.ContainsKey("password"))
.WriteTo.Console()
.CreateLogger();
IMP-03 — Add Security Scanning to CI/CD Pipeline
# .github/workflows/security.yml
- name: Secret scanning
uses: gitleaks/gitleaks-action@v2
- name: Dependency audit (.NET)
run: dotnet list package --vulnerable --include-transitive
- name: SAST
uses: github/codeql-action/analyze@v3
with:
languages: csharp, javascript
IMP-04 — Implement Refresh Token Rotation
Current refresh tokens have 7-day sliding window with no rotation. Add one-time-use refresh tokens:
// In ResourceOwnerPasswordValidator or token endpoint
// Revoke old refresh token on each use
await _tokenRevocationService.RevokeAsync(oldRefreshToken);
var newRefreshToken = await _tokenService.IssueRefreshTokenAsync(user);
IMP-05 — Add Referrer-Policy and Permissions-Policy Headers
Extend secure-headers middleware:
customResponseHeaders:
Referrer-Policy: "strict-origin-when-cross-origin"
Permissions-Policy: "camera=(), microphone=(), geolocation=(self), payment=(self)"
Cross-Origin-Opener-Policy: "same-origin"
Cross-Origin-Resource-Policy: "same-site"
Action Items
| Priority | ID | Action | Owner | Deadline |
|---|---|---|---|---|
| P0 | CRIT-01 | Rotate all production credentials, remove from git, purge history | DevOps + CTO | 24h |
| P0 | CRIT-02 | Revoke MCP server JWT token, remove .env from git tracking |
DevOps | 24h |
| P0 | CRIT-03 | Replace AddDeveloperSigningCredential() with cert-based signing for production |
Backend Lead | Before next deploy |
| P1 | WARN-01 | Evaluate HttpOnly cookie storage vs. localStorage for JWT tokens |
Frontend Lead | 1 week |
| P1 | WARN-02 | Add CSP header to Traefik secure-headers middleware |
DevOps | 1 week |
| P1 | WARN-03 | Separate CORS config by environment; remove localhost from production | DevOps | 1 week |
| P1 | WARN-04 | Create per-environment middlewares.yml; enforce sslRedirect: true in prod |
DevOps | 1 week |
| P1 | WARN-05 | Audit K8s manifests for RequireHttpsMetadata=false — must not appear in staging/prod |
DevOps | 1 week |
| P2 | WARN-06 | Disable Traefik insecure API, add BasicAuth to dashboard | DevOps | 2 weeks |
| P2 | WARN-07 | Apply rate limiting to tracking/pixel endpoints | Backend | 2 weeks |
| P2 | WARN-08 | Set explicit AllowedHosts in all service appsettings.json |
Backend Lead | 2 weeks |
| P2 | WARN-09 | Remove staging secrets.yaml from git, adopt External Secrets Operator |
DevOps | 2 weeks |
| P2 | WARN-10 | Add Redis-backed TOTP replay prevention | Backend | 2 weeks |
| P3 | IMP-01 | Implement full secrets management pipeline (Vault / External Secrets) | DevOps | 1 month |
| P3 | IMP-02 | Add Serilog sensitive data destructuring to all services | Backend | 1 month |
| P3 | IMP-03 | Add GitLeaks + CodeQL + dotnet vulnerability scan to CI/CD | DevOps | 1 month |
| P3 | IMP-04 | Implement refresh token rotation | Backend | 1 month |
| P3 | IMP-05 | Add Referrer-Policy and Permissions-Policy headers | DevOps | 1 month |
Positive Findings
The following security controls are correctly implemented and should be maintained:
- ✅ Account lockout: 5 failed attempts → 15-minute lockout (
DependencyInjection.cs:93-96) - ✅ TOTP 2FA: OtpNet library, 160-bit secret, QR code provisioning, 10 recovery codes
- ✅ Password hashing: ASP.NET Core Identity PBKDF2/SHA256, enforced complexity rules
- ✅ Audit logging: 52 event types tracked with IP, UserAgent, actor, resource (
AuditEventType.cs) - ✅ OTP hashing:
SHA256.HashData()used for phone/email OTPs — not stored in plaintext - ✅ Generic auth errors: Login failures return generic messages to prevent username enumeration
- ✅ Auth rate limiting: 10 req/min on login/register/2FA endpoints
- ✅ RBAC policies:
RequireSuperAdmin,RequireAdmin,OwnerOrAdmincustom handler - ✅ Health checks:
/health/liveand/health/readyendpoints on all services - ✅ Short-lived access tokens: 15-minute expiry
- ✅ Security headers:
X-Content-Type-Options,X-XSS-Protection,X-Frame-Options,HSTS - ✅ Input validation: FluentValidation in MediatR pipeline on all Commands
Report generated by Security Engineer — TechBi / GoodGo Platform Next audit scheduled: 2026-06-20