Skip to content

Secure coding guidelines

These are the secure coding practices required across every repository, project, and product owned by the Mobility Marketplaces team. They apply to every engineering role: developers, lead developers, and engineering managers.

This page is the canonical, team-level reference. Every repository MUST keep its own detailed, stack-specific security checklist, at .ai/security-checklist.md, and that file MUST link back here as its authoritative parent — add a security checklist to your repository has the template and the wiring. Part of the estate has one today; the Repository Security Coverage Matrix tracks which repositories still do not.

This page states the requirements. What actually runs, and where its results go, is on how supply-chain security works and how CI runs across our repos.

The key words MUST, MUST NOT, and SHOULD carry their RFC 2119 meanings. A MUST is non-negotiable and reviewable. A SHOULD may be set aside only with a documented reason.

Every restriction — who may see what, who may do what, which values are valid — MUST be enforced on the server.

Client-side gating, such as a hidden button, a disabled control, or a read-only prop, is user experience, not a security control. It MUST NOT be the only thing standing between a caller and an action.

Callers MUST be authenticated before authorization is evaluated. Authorization MUST NOT stand in for verifying who the caller is.

We delegate authentication to Almatunnus (our Auth0-based identity provider) rather than reimplementing it. A service MUST NOT invent its own password or token scheme.

Tokens and sessions MUST be validated in full on every request. For a JWT, validate the signature, the approved algorithm, expiry, issuer, audience, and any claim required for that token’s profile — a token issued for a different API must not pass. For a session, validate expiry and revocation state. Neither MUST be treated as already checked from an earlier request.

A public client (single-page app, mobile app) authenticating against Almatunnus MUST use the PKCE-extended authorization code flow, not the plain authorization code flow.

Tokens MUST NOT be stored in localStorage or any other location readable by page scripts.

Logout MUST invalidate the session at the identity provider, not only clear client-side state. We treat a token as revoked on password change or suspected compromise.

Every API boundary — a GraphQL resolver, a REST route, a CLI command with an external effect — MUST enforce authorization before performing the action.

Authorization MUST be evaluated against the specific resource, meaning its owner or the caller’s relationship to it, not only the caller’s role. A caller MUST NOT be able to reach another user’s record by supplying its identifier.

A state transition MUST verify, server-side, both that the caller has authority over the resource and that the transition is valid from the resource’s current state.

The authorization check MUST be explicit in the code, not inferred from context. A reviewer MUST verify it on every pull request that adds or changes one.

A privileged view of another user’s data, such as an admin or customer-care view, MUST be read-only unless a write capability is explicitly designed, reviewed, and justified.

Every privileged access to another user’s data MUST be audit-logged with the acting user, the record accessed, and the role it was viewed as.

Service credentials with elevated scope, such as internal API tokens or batch-job identities, MUST be scoped to least privilege. They MUST be used only from server-side jobs and service-to-service calls, and MUST NOT be reachable from a user-facing request path.

All externally supplied input — GraphQL arguments, REST payloads, query parameters, webhook and event payloads — MUST be validated for type, range, and format before use. Input MUST NOT be trusted implicitly because it originates from an authenticated session.

Validating input is not the same guarantee as handling output safely. Queries MUST be parameterized and never string-built from input. Output rendered into HTML, PDF, or another document format MUST be escaped for that context.

Inbound webhooks MUST verify the sender’s signature before the payload is trusted or acted on.

Redirect targets and outbound request URLs derived from user input MUST be validated against an allowlist. The application MUST NOT redirect to, or fetch from, an arbitrary user-supplied URL.

Sensitive fields MUST be guarded at the resolver, so a field is never returned to a caller not authorized for it, regardless of which query reached it.

GraphQL introspection MUST be disabled in production. Query depth and complexity MUST be limited so a single request cannot exhaust server resources.

Queries and GET requests MUST be side-effect free. Any state change MUST be a mutation or a non-GET request.

A state-changing request authenticated by cookie MUST carry CSRF protection — SameSite cookies together with a token or origin check.

Uploaded files MUST be validated by content, not by extension or declared MIME type, and MUST be size-limited.

Uploads MUST be stored outside the web root, in object storage with no public access, and served through the application or via short-lived signed URLs. Content that is public by design — a marketplace ad photo behind CloudFront — is the exception, and its bucket policy MUST still state exactly what is public.

A user-supplied value rendered into a template — Twig, PDF, email — MUST be escaped for that context. Template source MUST NOT be built from user input.

All network communication, including internal service-to-service calls, MUST use an approved TLS version and cipher suite, with certificate-chain and hostname validation — not merely TLS in some configuration.

Only vetted, standard cryptographic libraries and algorithms are allowed. We do not implement our own encryption, hashing, or random-number generation.

Sensitive identifiers, such as an SSN or a payment reference, MUST be encrypted at rest, or stored only as a keyed hash where lookup is the only need.

Secrets MUST NOT be committed to source control.

Secrets MUST come from a managed secrets store — AWS Secrets Manager or Parameter Store — injected as environment configuration at runtime. A committed .env or config file is not a secrets store. Local secret-scanning (git hooks) MUST remain enabled.

A workflow calling a reusable workflow MUST forward only the named secrets that workflow declares, and MUST NOT use secrets: inherit unless the called workflow’s header requires it. The caller contract in .github is the authority on that.

Read access to a secret MUST be scoped to the services that need it. Secrets SHOULD be rotated on a defined schedule, and MUST be rotated immediately on suspected exposure.

A field carrying PII, an SSN, or payment data MUST be explicitly identified in code and MUST NOT appear in logs or error messages. Access MUST be scoped to what the operation requires.

Every table or store containing PII or other sensitive data MUST have an explicit, documented retention policy with a stated justification for the chosen period, and the data MUST be removed immediately once that period ends. Absent a documented policy, the retention period defaults to 30 days from the point the data is stored, and the same immediate-removal rule applies once that default period elapses. This 30-day fallback is our own internal default, modeled on how quickly we’d need to act on a GDPR erasure request. It is not itself a GDPR-mandated retention limit, and it does not override a separate legal or regulatory retention requirement.

Data needed beyond our stated retention policy for a distinct, documented purpose — regulatory reporting, or internal analytics — MUST NOT remain in the active database under the general operational justification. It MUST instead live in a purpose-built store outside the active database that:

  • has its own justification, independent of the operational retention policy;
  • contains only the fields that purpose requires — a legal purpose may still require specific PII fields, but any other purpose MUST scrub PII;
  • is itself subject to retention review under that purpose’s own justification.

Production PII MUST NOT be copied into non-production environments or test fixtures. Test data MUST be synthetic or irreversibly anonymized.

DAC7 reporting in Baana is the example already in production: a dedicated AWS account and VPC, holding only the fields the directive requires — user_id, the tax identification number encrypted at rest, and a compliance state — under its own justification. The shape is described in baana-backend’s DAC7 integration doc. A new need for long retention should follow that same shape — purpose-built, minimal, separately justified — rather than extending retention in place. A store that predates this page should be audited against it rather than assumed compliant.

Amounts, currencies, and payment states MUST be taken from server-side records, never from the client and never from a callback payload alone. A callback MUST be reconciled against the stored transaction before any state change.

Payment callbacks and settlement operations MUST be idempotent. A replayed or duplicated notification MUST NOT release funds or advance state a second time.

An operation that is sensitive, that costs money per call, or that creates a legal or contractual liability MUST be rate-limited at the application level, per caller and per target, independent of infrastructure-level protection. This covers login, verification-code sending, identity lookups such as SSN checks, changes to contact details or credentials, and metered external lookups such as Trafi vehicle-data queries.

Every call to a metered or liability-bearing external service MUST be attributable in logs to a stable, non-sensitive actor or request identifier, so we can reconcile cost and detect anomalous volume. A raw email address, phone number, SSN, or payment identifier MUST NOT be used for that attribution — it would violate the PII, payment data, and retention rule above.

In login, password-reset, and account-recovery responses, a reply MUST NOT reveal whether an identifier — an account, an email, a phone number, an SSN — exists. A found and a not-found case MUST produce the same response. A lookup that is itself the feature, such as a Trafi vehicle query, is outside this rule but still inside the rate-limiting rule above.

Every repository, project, or product MUST have clearly defined, discoverable ownership: a named individual or team responsible for triaging dependency-scan findings, critical vulnerabilities, and license decisions. Ownership MUST be documented — a CODEOWNERS file, or a statement in the repository’s README or security checklist — not left implicit.

Dependencies MUST stay patchable through Dependabot or an equivalent, with automated scanning running in CI and results routed to that named owner.

A third-party GitHub Action MUST be referenced by commit SHA, never by tag or branch — a tag can be moved, a SHA cannot. An internal reusable workflow from .github is referenced at @main, unless that workflow’s own documentation requires a pinned SHA. The org CI policy is the authority; how CI runs across our repos explains why the two rules differ.

A dependency with a Restricted license (see License categories) MUST NOT be merged without documented approval from the engineering manager. The repository owner raises the request, naming the dependency, its version, the license identifier and the intended use, but cannot approve it. An approved Restricted license MUST then be recorded in the repository’s security checklist. A Prohibited or Unknown license additionally requires explicit written approval from security and legal — engineering-manager approval alone is not sufficient for those two.

An authentication failure, an authorization failure, a privileged access (see Privileged and service access), and an unhandled server-side failure MUST be logged with enough context to investigate — a non-sensitive actor or request identifier where known, the endpoint, and the reason — without logging the credential, the token, or a raw PII value.

A security event MUST be distinguishable from routine application logs, so it can be found and alerted on rather than lost in general noise.

An error response returned to a caller MUST NOT include a stack trace, internal query text, a file path, or other implementation detail. Full detail belongs only in server-side logs, never the response body.

An unexpected exception raised during a security-relevant check — authentication, authorization, input validation — MUST cause the action to be denied. A check MUST fail closed, never open, when it cannot complete.

Code produced with AI assistance is held to every requirement on this page and MUST pass through the same review and tooling as any other code.

Credentials MUST NOT be provided to an AI tool, in a prompt or as attached context — including inside a pasted stack trace, log line, or .env you meant to trim.

What may go into a prompt, which tools are licensed, and what shipping an AI feature obliges are settled in Alma AI policy for developers, with the production-data conditions alongside them. Those are the authority; this page does not restate them.

Wherever a requirement on this page can be checked systematically, it MUST be enforced by tooling — a CI gate, a linter, a SAST rule — rather than left to a reviewer’s attention.

The reviewer’s job is to confirm the code adheres to our consensus-based standards, this page included, and to serve as the last line of defense precisely where tooling can’t reach. On every pull request touching an API boundary, the reviewer MUST treat missing authorization or missing input validation as a blocking finding, not a suggestion.

Every repository in scope MUST maintain its own security checklist with repository-specific guidance, linking back to this page as its authoritative parent. Follow add a security checklist to your repository.

Every repository MUST have CI-enforced secret scanning, SAST, and dependency auditing as required, non-bypassable status checks. The Repository Security Coverage Matrix holds the per-repository status, and a control marked Planned or Missing there MUST have an owner and a target date.

A repository owner is responsible for evaluating their own repository against this page and closing any gap directly. This page states the requirement, and the Coverage Matrix tracks status.

Exceptions. Where a requirement cannot be met, the deviation MUST be recorded in the repository’s security checklist with the reason, the compensating control, the approving owner, and a review date. A deviation covering a Critical or High finding MUST additionally be recorded in the Mobility continuity risk assessment, with the engineering manager notified — the repository’s own checklist does not reach the risk review. A deviation MUST NOT be open-ended: it is re-examined at every quarterly review and expires unless renewed.

A finding from SAST, secret scanning, dependency auditing, or code review is handled by severity. The clock starts when the finding is first detected.

Severity Merge Resolve or record an exception within
Critical Blocked 24 hours
High Blocked 14 days
Medium Allowed 30 days
Low Allowed Backlog, as capacity allows

The Merge column is the target state. A secret-scanning or SAST finding blocks a merge today, because those are required status checks. A dependency-audit finding does not, so there the reviewer remains the only gate — see the caution under Repository compliance.

A Critical or High finding that is neither resolved nor covered by a recorded exception within its window MUST be escalated to engineering management, and recorded in both places named under Repository compliance: the repository’s security checklist and the Mobility continuity risk assessment.

A CI gate that exists but is not a required status check does not count as enforcement. Disabling or bypassing a gate MUST itself be recorded as an exception.

Category Examples Usage
Permitted MIT, Apache-2.0, BSD-2/3-Clause, ISC, Unlicense Usable freely, no review needed
Restricted LGPL-2.1/3.0, MPL-2.0, CDDL-1.0, EPL-2.0, GPL-2.0/3.0 Requires documented review and approval
Prohibited AGPL-3.0, SSPL-1.0, BUSL-1.1 Requires explicit written security/legal approval
Unknown No declared license Treated as Prohibited until clarified

Quarterly — a repository owner self-reviews against this page, the Coverage Matrix, and any open exceptions.

Annual — engineering management runs a full compliance review across all repositories.