Skip to content

Security & Access Control

This page documents how the system authenticates users, authorises actions, and audits them — as built. Several mechanisms here are weak by modern standards; they are described plainly so support and any future hardening work start from the truth. The weaknesses are collected on Known Issues.

Login flow

flowchart TD
    A["User enters UserID + Password"] --> B{"UserID = EDP or DLY?"}
    B -->|Yes| C["Backdoor: password = time-derived key<br/>(digits of dd-MM-yyyy HH:mm:ss)<br/>no DB record — full access"]
    B -->|No| D["SELECT * FROM tbMaster_User<br/>WHERE UserID = UCASE(input)"]
    D --> E{"PassToChar(UserPassword) = input?"}
    E -->|No| F["Reject"]
    E -->|Yes| G{"RECORDID?"}
    G -->|0 active| H["Login OK"]
    G -->|1 first login| I["Force password change"]
    G -->|2 disabled / else| J["Blocked / expired"]
    H --> K["Enforce single session per station<br/>(tbMaster_Computer.UserAktif)"]
    style C fill:#ffebee,stroke:#c62828
  • Normal users are validated against tbMaster_User; the stored password is run through PassToChar (a reversible cipher, see below) and compared to the input.
  • RECORDID encodes account state: 0 active, 1 first-login (force change), 2 disabled, otherwise expired. On success, USERLASTLOGIN is updated.
  • Single session per station is enforced via tbMaster_Computer.UserAktif; clearing a stuck session needs an approval (AccessCode = "RESETUSER").

Two hard-coded superuser backdoors

Accounts EDP and DLY have no database row. Their password is a time-derived key built from digits of the current timestamp (dd-MM-yyyy HH:mm:ss). Anyone who knows the algorithm and the server clock can log in as a full superuser. These accounts also bypass every permission check below.

Authorisation: per-form rights

Authorisation is per-form (per-URL) add/edit/delete, not role-based.

flowchart LR
    U["User opens a form (URL)"] --> BA["ButtonAccess(UserID, URL, ByRef Tambah, Koreksi, Hapus)"]
    BA --> Q["JOIN tbMaster_UserAccess × tbMaster_Access<br/>WHERE UserID=… AND URL=…"]
    Q -->|row found| R["Set Tambah/Koreksi/Hapus from bit columns"]
    Q -->|no row, EDP/DLY| S["True / True / True"]
    Q -->|no row, other| T["False / False / False"]
    Q -->|SQL exception| FO["⚠️ True / True / True (fail-open)"]
    style FO fill:#ffebee,stroke:#c62828
    style S fill:#fff3e0,stroke:#fb8c00
  • ButtonAccess(UserID, URL, ByRef Tambah, Koreksi, Hapus) joins tbMaster_UserAccess × tbMaster_Access on AccessCode for the given user + form URL, and returns the three permission bits (Tambah=add, Koreksi=edit, Hapus=delete; Baca=read gates visibility).
  • No grant rowEDP/DLY get full access; everyone else gets none.
  • Menu visibility is gated separately by HAKFUNC.UserAccessMenu (the menu tree is filtered per user, except EDP/DLY who see everything); the button strip uses AccessCode = 'SHOWBUTTON'.

ButtonAccess fails open

On any SQL exception, ButtonAccess returns True/True/True — granting add, edit, and delete. A transient DB error therefore elevates permissions rather than denying them. This is the opposite of fail-safe.

The access tables

Table Holds
tbMaster_Access Form/menu catalogue: AccessGroup+AccessCode, AccessName, URL, permission flags Baca/Tambah/Koreksi/Hapus, tree RootID.
tbMaster_UserAccess Per-user grants: UserID+AccessGroup+AccessCode, with Baca/Tambah/Koreksi/Hapus bits.
tbMaster_User The user record: USERID, USERPASSWORD (ciphered), USERLEVEL, STATION, RECORDID (state), USERLASTLOGIN.

Password & credential storage

Passwords use a reversible cipher, not a hash

Convertion.PassToChar / CharToPass is a Caesar-style per-character shift (nTambah = 57) plus StrReverse. It is fully reversible — anyone with the stored value (or the algorithm) can recover the plaintext. The same cipher also protects the SQL Server credentials in the Windows Registry, so DB username/password are recoverable too. Neither user passwords nor DB credentials are truly secret. Modern practice would be bcrypt/PBKDF2 for user passwords and OS-level secret storage for credentials.

Audit trail

Mutations and access events are logged to AuditTrail_UserAccess via the FrameWork's shared AuditTrail(Tipe) routine.

Column Holds
…KodeCabang Branch
…Userid Who
…ActionDate When
…Application Which app
…Modul Which module/form
…Action Login, Open Menu, or Insert/Edit/Hapus (Tipe ½/3)
…Description Details
…IP Workstation IP

MenuUtama writes a Login row on sign-in and an Open Menu row when a menu/EXE is opened; the typed data-access SaveData paths write Insert/Edit/Hapus rows. Note the INSERT is string-concatenated like the rest of the data layer.

Licensing

Startup runs a licence/expiry gate (for non-POS apps): it reads an "Expired Date" from REF_SYSTEM, and if expired calls the cloud licence API at https://license.smartsoft.co.id/license?client_id=<branch>. On failure it can blank the server name (blocking use) and pop the settings dialog. CD-key validation lives in MyLib.CdKey.

Summary of security-relevant behaviours

Behaviour Risk
EDP / DLY time-based backdoor superusers Undocumented full-access accounts.
ButtonAccess fails open on SQL error DB hiccup can elevate privileges.
Reversible-cipher passwords and DB credentials Both recoverable from stored values.
String-concatenated SQL everywhere SQL-injection exposure (see Data Access).
Credentials embedded in the connection string Cleartext in memory / registry-obfuscated only.

See Known Issues & Risks for the consolidated list and context.