Security Testing Policy
Security Testing Policy
Project / Organization:
BilkoALAI Holding AS —BalkanDropAccountingPaymentSaaSApp Policy Number: POL-SEC-TEST-001 Version: 1.0 Date: 2026-02-23 Author:ComplianceSecurity Architect Status: Draft Reviewers:CTO,CISO, EngineeringLeadLead, CTO Next Review:2026-08-2027-02-23 Classification: Confidential
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | Initial draft — |
1. Purpose & Scope
Purpose: This policy defines the security testing methodology, tools, frequency, and remediation requirements for all systems operated by Bilko.ALAI Holding AS for the Drop payment app. Security testing is mandatory — no system goes to production without completing applicable security tests.
Regulatory basis:
- IKT-forskriften (FOR-2003-05-21-630) §§ 5-6 — ICT security verification
- DORA (EU) 2022/2554 Art. 24-25 — Digital operational resilience testing
- Finanstilsynet licensing requirements — penetration test before production launch
- GDPR Art. 32(1)(d) — regular testing of technical measures
Scope:
- All production
applications:APIsbilko.ioand endpoints (Vercel)getdrop.no,api.)bilko.io (Railway)getdrop.no PostgreSQLAlldatabaseAWS infrastructure (RailwayAppEURunner,West)S3, CloudflareKMS,R2Secretsfile storageManager)- All CI/CD pipelines and build systems
Third-All third-partyintegrations: SEFintegrations (Serbia),BankID,HR-FISK/FINASumsub,(Croatia)Open Banking partners)- Developer workstations handling Confidential or Restricted data
Policy Owner: Compliance ArchitectCISO ([email protected])[email protected])
Operational Owner: Security team + Engineering Lead
Regulatory basis:
GDPR Art. 32 — "regular testing, assessing and evaluating the effectiveness of technical and organisational measures"ZZPL Art. 50 (Serbia) — security testing obligationZakon o računovodstvu — integrity of financial processing systems
2. Security Testing Methodology
Approach: Shift-left security — testing integrated throughout development, not bolted on at the end.
Testing layers:
Development Phase
├──+-- SASTUnit security tests — Static code analysisVitest (everycurrent: commit)20+ ├──security-specific tests)
+-- SCA — Dependencynpm vulnerability scanaudit (every commit)commit, └──automated)
+-- Secret scanning — Detectdetect leaked credentials (every commit)
Build / CI Phase
├──+-- SAST — Fullstatic codebasecode scananalysis (every PR — planned Phase 2)
+-- SCA — dependency vulnerability check (every PR)
├── SCA — Full dependency audit (every PR)
└──+-- Secret scanning — Full repofull scan (every PR)
Deployment Phase
├──+-- DAST — OWASP ZAP scan against staging (everyplanned deployment)— └──Phase 3)
+-- API security:security Org-scopescan isolation— testsendpoint fuzzing (everyplanned deployment)— Phase 3)
Operational Phase
├──+-- Vulnerability assessment — Externalexternal attack surface (quarterly)
└──+-- Penetration test — Manualmanual expert testing (annual,before Phase 2+)3 launch)
+-- Post-incident review — regression tests after any security incident
3. Current Security Test Coverage
3.1 Vitest Unit Security Tests
Status: Implemented — 20+ security-specific tests
Source: src/drop-app/src/__tests__/
| Test Category | Test | Source |
|---|---|---|
| Authentication | JWT secret required in production | auth.test.ts |
| Authentication | Cookie: httpOnly=true, secure=true, sameSite=strict | auth.test.ts |
| Authentication | JWT contains setIssuedAt() | auth.test.ts |
| Authentication | Session revocation: revoked token rejected | auth.test.ts |
| Authentication | Logout revokes all user sessions | auth.test.ts |
| Password | bcrypt used — not SHA-256 | utils-server.test.ts |
| Password | bcrypt cost factor >= 12 | utils-server.test.ts |
| Password | SHA-256 hash rejected as invalid (fix C4) | utils-server.test.ts |
| Input Validation | IBAN checksum validation | validation.test.ts |
| Input Validation | Currency whitelist enforced | validation.test.ts |
| Input Validation | Language whitelist enforced | validation.test.ts |
| Input Validation | Amount: NaN/Infinity rejected | validation.test.ts |
| Input Validation | HTML tags stripped by sanitizeText() | validation.test.ts |
| SQL Injection | Parameterized queries for all user inputs | db.test.ts |
| Rate Limiting | Auth rate limit: 10/60s enforced | middleware.test.ts |
| Rate Limiting | Transaction rate limit: 10/60s enforced | middleware.test.ts |
| CSRF | Origin header validated | middleware.test.ts |
| CSRF | Invalid origin rejected | middleware.test.ts |
| IDOR | Transaction query scoped to user_id | transactions.test.ts |
| IDOR | Recipient query scoped to user_id | recipients.test.ts |
| Feature Flags | Cards endpoint returns 404 when flag disabled | feature-flags.test.ts |
3.2 Running the Security Test Suite
# Run all security tests
cd src/drop-app && npx vitest run --reporter verbose
# Run specific security test file
npx vitest run src/__tests__/auth.test.ts
# Run with coverage
npx vitest run --coverage
Blocking criteria: All security tests MUST pass before merge to main branch.
4. Testing Types, Tools & Schedule
3.4.1 SAST — Static Application Security Testing
Purpose: Detect security vulnerabilities in TypeScript/JavaScript source code.
| |
SAST rules in scope:
OWASP Top 10 (A01-A10)SQL injection patternsXSS patterns (dangerouslySetInnerHTML without sanitization)Hardcoded secrets / API keysInsecure JWT usage (no algorithm verification, expired tokens not rejected)Raw SQL in Prisma ($queryRawwith user input)Missing org-scope WHERE clausesNo-eval, no-unsafe-regex rules
ESLint configuration:
{
"extends": ["plugin:security/recommended"],
"rules": {
"security/detect-object-injection": "warn",
"security/detect-non-literal-regexp": "error",
"security/detect-possible-timing-attacks": "error",
"no-eval": "error",
"@typescript-eslint/no-explicit-any": "error"
}
}
3.2 DAST — Dynamic Application Security Testing
Purpose: Detect runtime vulnerabilities by sending attack traffic to a running instance.
DAST scan scope for Bilko:
All API endpoints (authenticated + unauthenticated)Authentication flows: login, register, password reset, 2FAInvoice CRUD endpoints (create, update, delete)Expense endpointsVAT report generationFile upload (receipt attachments)SEF/HR-FISK submission endpointsOrg-scope isolation (cross-tenant access attempts)Security headers presence and correctnessSSL/TLS configuration
Critical Bilko-specific DAST tests:
1. Cross-tenant IDOR test:
- Login as org A user
- Attempt GET /api/v1/invoices/{invoice-id-from-org-B}
- Expected: 403 Forbidden
- If 200: CRITICAL finding — immediate halt
2. RBAC bypass test:
- Login as accountant role
- Attempt POST /api/v1/invoices (create)
- Expected: 403 Forbidden
- Attempt DELETE /api/v1/invoices/{id}
- Expected: 403 Forbidden
3. SQL injection via Prisma:
- Attempt org-scope bypass via invoice filter parameters
- Expected: Prisma parameterization prevents injection
4. Tax ID in response check:
- Verify tax IDs returned as masked/encrypted, not plaintext
- Verify tax IDs not present in error responses or logs
3.3 SCA — Software Composition Analysis
Purpose:Status: IdentifyImplemented known(npm vulnerabilities in open source dependencies.audit)
| Property | Value |
|---|---|
| Tool | npm audit (built-in) + GitHub Dependabot ( |
| Frequency | Every commit (PR |
| Blocking | YES — |
Current dependency security status (unresolved)2026-02-13 audit):
| Package | Version | Risk | Status |
|---|---|---|---|
jose |
^6.1.3 | Low | No known CVEs |
bcryptjs |
Low | No known CVEs | |
better-sqlite3 |
^12.6.2 | Low | No known CVEs |
next |
16.1.6 | Low | Recent version |
react |
19.2.3 | Low | Latest major |
radix-ui |
^1.4.3 | Low | UI only — |
Source: ~/ALAI/products/Drop/security/drop-security-rapport.md
Dependency update policy:
- Security patches (Critical/
High CVE)High): Merge within24hSLA/(see7 days per remediation SLA§5) - Minor security updates: Merge within 14 days
- Major updates: Planned migration within 90 days
Approved licenses: MIT, Apache 2.0, BSD (2-clause, 3-clause), ISC
Requires review: GPL, LGPL, AGPL
Current
Bilko4.2
key dependencies to monitor:
prisma (ORMSAST — SQLStatic injection prevention)
jsonwebtoken / jose (JWT — auth bypass if vulnerable)
bcrypt / bcryptjs (password hashing — compromise risk)
zod (input validation — bypass risk)
express (web framework — RCE risk)
3.4 VitestApplication Security TestsTesting
Purpose:Status: Bilko-specificPlanned automated(Phase security regression tests for accounting isolation.2)
| Property | Value |
|---|---|
| Tool | |
| Frequency | Every |
| Languages | TypeScript, JavaScript |
| Blocking | YES — |
RequiredSAST securityrules testin cases:scope (when implemented):
- SQL injection detection (parameterized queries validation)
- JWT algorithm confusion (
detection)/alg: none - Hardcoded secrets /
1.credentials - Insecure
testscryptography (MD5, SHA-1, DES, RC4) - Path traversal / LFI
- IDOR patterns (database queries without user_id scoping)
- Missing authentication middleware
4.3 DAST — allDynamic role/actionApplication combinationsSecurity describe('RBAC',Testing
Status: Planned ()Phase => {
test('accountant cannot create invoice', async () => {
const res = await api.post('/api/v1/invoices', invoice, { role: 'accountant' });
expect(res.status).toBe(403);
});
test('viewer cannot generate report', async () => {
const res = await api.get('/api/v1/reports/vat', { role: 'viewer' });
expect(res.status).toBe(403);
});
});
// 2. Org-scope isolation tests
describe('Multi-tenant isolation', () => {
test('org A cannot access org B invoice', async () => {
const orgBInvoice = await createInvoice({ org: 'org-b' });
const res = await api.get(`/api/v1/invoices/${orgBInvoice.id}`, { org: 'org-a' });
expect(res.status).toBe(403);
});
test('org A cannot list org B contacts', async () => {
const res = await api.get('/api/v1/contacts', { org: 'org-a' });
expect(res.body.data.every(c => c.organizationId === 'org-a-id')).toBe(true);
});
});
// 3. VAT calculation accuracy tests
describe('VAT calculations', () => {
test('Serbian VAT at 20%', () => {
expect(calculateVAT(new Decimal('100.00'), 'RS')).toEqual(new Decimal('20.00'));
});
test('Croatian VAT at 25%', () => {
expect(calculateVAT(new Decimal('100.00'), 'HR')).toEqual(new Decimal('25.00'));
});
test('BiH VAT at 17%', () => {
expect(calculateVAT(new Decimal('100.00'), 'BA')).toEqual(new Decimal('17.00'));
});
test('NUMERIC precision3 — nobefore floatproduction rounding',launch)
| Property | Value |
|---|---|
| Tool | OWASP ZAP ( |
| Frequency | Every |
| Target | https://staging.getdrop.no |
| Blocking | YES — deployment halted if Critical finding discovered |
DAST scan scope:
- All 24 API endpoints (authenticated + unauthenticated)
- Authentication flows (login, registration, logout, session handling)
- Rate limiting (verify 10/60s limits enforced)
- CSRF protection (Origin header validation)
- Input validation
tests(IBAN,describe('Zodcurrency,validation',language, amount) - Security headers presence (HSTS, CSP, X-Frame-Options, etc.)
- SSL/TLS
{ test('rejects non-UUID customerId', asyncconfiguration ()TLS=>1.3,{noconstTLSres1.0/1.1) - JWT
await api.post('/api/v1/invoices', { customerId: 'not-a-uuid' }); expect(res.status).toBe(400); }); test('rejects future invoice date beyond 1 year', asynchandling ()expiry,=>algorithm,{revocation) - Feature
resflag=enforcementawait(cardsapi.post('/api/v1/invoices',endpoints{returninvoiceDate:404'2030-01-01'when});disabled)
3.54.4 Penetration Testing
Purpose:Status: ExpertRequired manualbefore securityPhase assessment.3 production launch (Finanstilsynet licensing requirement)
| Property | Value |
|---|---|
| Frequency | |
| Methodology | OWASP Testing Guide v4.2 + PTES |
| Provider | External security firm (not yet selected) |
| Blocking | Critical findings: system taken offline until remediated |
Penetration test scope for Bilko:scope:
In-scope:
- Production:Staging pre-launch: https://bilko.io,staging.getdrop.no
- Production (after launch): https://getdrop.no, https://api.bilko.iogetdrop.no
- Authentication flows (login, register,registration, 2FA,BankID passwordOIDC reset)callback — Phase 2)
- All 24 authenticated API endpoints
- Multi-tenantSession isolationmanagement (IDORJWT, acrosscookie organizations)handling, revocation)
- FileRate uploadlimiting (receiptand attachments)CSRF protection
- SEFAWS andApp HR-FISKRunner submission endpointsconfiguration
- FinancialCloudflare calculationWAF integrity (VAT, double-entry)configuration
Out-of-scope:
- Denial of service attacks
- RailwaySocial andengineering Vercelof staff without prior approval
- AWS infrastructure itself (cloudAWS providerglobal responsibility)
- CloudflareBankID infrastructureNorge AS systems
- Social engineering
- Third-party SEF/FINASumsub systems
Rules of engagement:
- Testing window: AgreedTo be agreed in writing with provider
- Notify before engagementtesting: [email protected]
- Halt on critical finding: Immediately notify [email protected][email protected] -+ Test accounts provided: Separate test organizations in stagingCTO
Key test areas for Drop:
- JWT manipulation (algorithm confusion, expiry bypass, forged tokens)
- Session revocation bypass (sessions table manipulation)
- IDOR attacks (user_id scoping validation across all 24 endpoints)
- Rate limiting bypass (IP spoofing via X-Forwarded-For)
- CSRF (Origin header bypass)
- SQLi in all 24 endpoints (parameterized query verification)
- BankID OIDC callback manipulation (Phase 2)
- Feature flag bypass (cards endpoints when disabled)
- Foedselsnummer exposure (ensure never in plaintext logs or API responses)
4.5. Vulnerability Classification & Remediation SLAs
4.5.1 Severity Classification (CVSS v3.1)
| Severity | CVSS Score | Drop-Specific Definition | |
|---|---|---|---|
| Critical | 9.0-10.0 | ||
| High | 7.0-8.9 | Privilege escalation, IDOR (cross- | |
| Medium | 4.0-6.9 | ||
| Low | 0.1-3.9 | ||
| Informational | N/A | Best practice |
4.5.2 Remediation SLAs
| Severity | SLA | Action on Breach of SLA |
|---|---|---|
| Critical | Emergency — CISO + CTO + CEO. System taken offline if risk cannot be |
|
| High | 7 calendar days | Escalate to |
| Medium | 30 calendar days | Engineering |
| Low | 90 calendar days | |
| Informational | Next sprint (best effort) | Not SLA-bound. |
Cross-tenantSLA IDOR (High+) is treated as Critical for Bilkotracking: —GitHub financialIssues datawith exposurelabel forsecurity-finding any+ organizationseverity triggers immediate P1 incident response.label.
5.6. Security Code Review Checklist
Required for every PR touching authentication, RBAC,authorization, financialpayment calculations,processing, PII, or PII:cryptography:
Authentication & Authorization:
- No hardcoded
secretscredentials,inJWTcodesecrets, or API keys - Password hashing uses bcrypt (cost >= 12) — SHA-256 explicitly rejected (fix C4)
- JWT
validated:validation: signature, expiry,iss,iataudclaim verified viajose -
org-scopedSessionWHERErevocation:clauseallonprotectedeveryendpointsPrismacheckquerysessionstouchingtablemulti-tenantfordatarevoked = 0 -
RBACNorolerequireRole()middlewareflagsappliedfromonuser-suppliedevery protected endpointinput -
2FAAllcodeprotectedvalidationendpointsuseshaveconstant-timeauthenticationcomparisonmiddleware applied - Logout revokes sessions server-side (not just clears cookie)
Input Validation:
-
ZodAllschemauserforinputsallvalidatedrequestusingbodiesDrop validators (validateAmount, validateIBAN, validateCurrency, validateLanguage, sanitizeText) -
PrismaAll database operations use parameterized?queriesonly— nostring$queryRawwith user inputconcatenation -
FileUseruploads:datatypequeriesvalidated,includestoredANDinuser_idR2=not?webrootscoping (IDOR prevention) - No
stringeval(), exec(), or shell command interpolationinwithSQLuseror system commands
Financial Integrity:
All monetary values useDecimal/NUMERIC— nonumberorfloatVAT rates validated against country code (not user-supplied)Double-entry: debit = credit enforcedExchange rates locked at transaction dateinput
Cryptography:
- No MD5, SHA-1, DES, RC4
anywhere(per data-encryption-policy.md §2.2) - Random values
fromusecrypto.randomBytes()— notMath.random() -
bcryptIVs/noncescostarefactorrandom=(96-bit12minimum)forandanynevernew password hashingreused -
PrivateNo cryptographic keys/ secrets notin sourcecodecode, logs, orlogserror messages
Error Handling:
- No stack traces or internal paths in API error responses
- Generic messages for authentication failures (no user enumeration)
- Errors logged to Sentry internally but not exposed in API responses
Data Handling:
-
TaxFoedselsnummerIDsnot logged in plaintext (PIB/JMBG/OIB/JIB)neverandinIBANSentry,encryptedBetterStack,before storageconsole.log) - Sensitive data not in query parameters
or(useURLsPOST body) -
PIIFeaturenotflagsincheckedSentrybeforeerroranyreportscards-related(use.addEventProcessor()to scrub)endpoint -
LoggedActionAMLentriesretention:foruseralldatafinancialdeletionmutationsrespects 5-year retention (Hvitvaskingsloven § 30)
Dependencies:
- New dependencies reviewed with
npm auditbefore adding - No end-of-life packages introduced
- License compatible (MIT, Apache 2.0, BSD, ISC)
6.7. Security Testing in CI/CD Pipeline
Current pipeline (MVP):
Developer Commit
+-- Pre-commit hook: npm audit (SCA)
|
v
Git Push -> PR
|
v
Vitest security tests (20+ tests)
|
v (all pass)
Merge to main
|
v
Deploy to AWS App Runner
Target pipeline (Phase 2+):
flowchart LR
COMMIT[Developer\nCommit] -->|Pre-commit| SAST_INC[ESLint\nSASTSCA_INC[npm incremental]audit\nSCA SAST_INCcheck]
SCA_INC -->|Pass| PUSH[Git Push]
PUSH --> PR[Pull Request]
PR --> SAST_FULL[ESLintVITEST[Vitest\nSecurity Full\n+ TypeScript strict]Tests]
PR --> SCA[SAST[SAST\nSemgrep/CodeQL]
PR --> SCA_FULL[npm audit\nDependabot]nFull scan]
PR --> SECRET[Secret\nScanning]
PR --> VITEST[Vitest\nSecurity tests]
SAST_FULLVITEST & SCASAST & SCA_FULL & SECRET & VITEST -->|All pass| BUILD[Build]Build\nDrop App]
BUILD --> STAGING[Deploy Staging]to\nStaging]
STAGING --> DAST[OWASP ZAP\nDAST scan]
STAGING --> E2E[Playwright\nIsolation tests]Scan]
DAST & E2E -->|Pass| GATE{Security Gate}Security\nGate}
GATE -->|Pass| PROD[Deploy Production]to\nProduction]
GATE -->|Fail| BLOCK[Block Deploy\nAlert [email protected]][email protected]]
Pipeline security gate criteria:
| Gate | Tool | Blocking Criteria |
|---|---|---|
| Pre-commit | ||
| npm audit | Critical CVE | |
| PR gate | ||
| Vitest security tests | Any test failure | |
| PR gate 2 | SAST (Phase 2) | Critical or High finding |
| PR gate 3 | npm audit full | Critical CVE |
| PR gate 4 | Secret scanning | Any detected secret |
| Post-staging | Critical or High dynamic finding |
8. Security Audit History
| Date | Type | Provider | Findings | Status |
|---|---|---|---|---|
| 4C / 5H / 6M / 4L | Complete | |||
| 2026-02-13 | Hardening verification | Internal | 0C / 0H / 2M / 4L | Complete |
| TBD (Phase 3) | External penetration test | TBD | — | Planned — required before launch |
| TBD (Phase 3+) | Annual pentest | TBD | — | Annual thereafter |
Source: ~/ALAI/products/Drop/security/drop-security-rapport.md
Security Hardening Summary (2026-02-13)
| Finding | ID | Status |
|---|---|---|
Card data: only last_four + token_ref stored (no PAN/CVV) |
C1 | Resolved |
Demo credentials gated behind NODE_ENV !== 'production' |
C2 | Resolved |
| SHA-256 password support removed — bcrypt only | C4 | Resolved |
| Session revocation implemented and active | C6/H1 | Resolved |
| Input sanitization applied to all text fields | H4 | Resolved |
| Notification IDs validated (max 100, format check) | M5 | Resolved |
| Settings: currency/language validated against whitelists | M6 | Resolved |
Remaining items:
| Finding | ID | Type | Plan |
|---|---|---|---|
CSP: unsafe-inline/unsafe-eval required by Next.js |
M1 | Medium | Nonce-based CSP in Phase 3 |
Proxy HOSTNAME config |
M2 | Medium | Resolve in Phase 2 AWS config |
7.9. Reporting Format
9.1 Individual Finding
Finding ID: VULN-{YEAR}-{SEQUENCE}
Title: {SHORT_DESCRIPTION}
Severity: Critical / High / Medium / Low
CVSS Score: {SCORE} (v3.1)
Description:
{DETAILED_DESCRIPTION}
Affected Endpoint / File:
- {ENDPOINT_OR_FILE}: {URL_OR_LINE_NUMBER}
Proof of Concept:
{STEPS_TO_REPRODUCE}
Impact:
{WHAT_AN_ATTACKER_CAN_DO}
GDPR/AML Implications:
{IF_PERSONAL_DATA_OR_FINANCIAL_DATA_AFFECTED}
Remediation:
{SPECIFIC_FIX}
Owner: {ASSIGNED_ENGINEER}
SLA Due Date: {DATE}
Status: Open / In Progress / Resolved / Accepted Risk
10. Metrics & KPIs
| Metric | Target | Reporting Frequency |
|---|---|---|
| Critical findings resolved within SLA | 100% | Monthly |
| High findings resolved within SLA | Monthly | |
| Vitest security test pass rate | 100% (blocking) | Every PR |
| MTTR — Critical | < 24 hours | Per incident |
| MTTR — High | < 7 days | Monthly |
| Monthly | ||
| npm audit |
0 in production | |
11. Bug Bounty Program
Security metrics tracking:Status: GitHubPlanned Issues(Phase with3 — securitylabelpost-launch)
QuarterlyPlatform: Intigriti or HackerOne
Responsible disclosure (interim — before bug bounty):
Report security report: Publishedvulnerabilities to CEO[email protected].
- Acknowledgment:
teamwithin 1 business day - Triage: within 5 business days
- Safe harbor: researchers acting in good faith are protected from legal action
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | 2026-02-23 | ||
| Engineering Lead | |||
| Management |