---
title: "QuickBooks App Security Review Requirements Checklist"
description: "Every QuickBooks Online app security-review requirement in plain terms, with a command to check each one and fixes for Express, Next.js, nginx, Django and Rails."
url: "https://www.apideck.com/tools/quickbooks-review/checklist"
section: "Tools"
---

# QuickBooks App Security Review Requirements Checklist

Every QuickBooks Online app security-review requirement in plain terms, with a command to check each one and fixes for Express, Next.js, nginx, Django and Rails.

[QuickBooks app review check](https://www.apideck.com/tools/quickbooks-review) / Requirements guide

## The QuickBooks app security review, item by item

What each requirement asks, why reviewers care, how to check it yourself, and how to fix it in common stacks. Or [run the free check](https://www.apideck.com/tools/quickbooks-review) on your app.

This is our plain-language reading of Intuit's published requirements, not official guidance. Requirements change, so confirm the current list in Intuit's developer documentation before you submit.

## Server & transport

### HTTPS enforced on every pageAutomated

All pages of the app must be served over HTTPS.

Every page, including marketing pages on the same host, should only be reachable over HTTPS. Plain HTTP should redirect permanently.

#### Check it yourself

```
curl -sI http://app.example.com | grep -iE "^(HTTP|location)"
# expect: HTTP/1.1 301 … and location: https://…
```

#### Fix it

nginx

```
server {
  listen 80;
  server_name app.example.com;
  return 301 https://$host$request_uri;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
```

Express

```
app.set('trust proxy', 1);
app.use((req, res, next) =>
  req.secure ? next() : res.redirect(301, `https://${req.headers.host}${req.url}`));
```

### TLS 1.1 minimum, TLS 1.2+ recommendedAutomated

SSL must support TLS 1.1 or higher; TLS 1.2 with AES-256 and SHA-256 is recommended.

TLS 1.0 is below the minimum, and TLS 1.1 is deprecated everywhere else. Most managed platforms (Vercel, Cloudflare, AWS ALB with a modern policy) already refuse both.

#### Check it yourself

```
openssl s_client -connect app.example.com:443 -tls1_1 </dev/null 2>&1 | grep -E "Protocol|alert"
# a handshake failure here is what you want
```

#### Fix it

nginx

```
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
```

AWS ALB

```
Use the security policy ELBSecurityPolicy-TLS13-1-2-2021-06 (or newer) on the HTTPS listener.
```

### Caching disabled on sensitive pagesAutomated

Use Cache-Control no-cache and no-store (not private) on SSL and sensitive pages.

\`private\` only stops shared caches; the browser can still store the page on disk. Anything showing QuickBooks data, tokens or account settings needs \`no-store\`.

#### Check it yourself

```
curl -sI -H "Cookie: session=…" https://app.example.com/dashboard | grep -i cache-control
# expect: no-store (and no-cache)
```

#### Fix it

Express

```
app.use(['/app', '/oauth'], (req, res, next) => {
  res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
  res.set('Pragma', 'no-cache');
  next();
});
```

Next.js

```
// next.config.js
headers: async () => [{
  source: '/app/:path*',
  headers: [{ key: 'Cache-Control', value: 'no-store, no-cache, must-revalidate' }],
}]
```

Django

```
from django.views.decorators.cache import never_cache

@never_cache
def dashboard(request): ...
```

### TRACE and unused HTTP methods disabledAutomated

The web server must disable TRACE and other HTTP methods that are not used.

TRACE echoes the request back, including headers, which enables cross-site tracing. Turn off any method your app does not serve.

#### Check it yourself

```
curl -si -X TRACE https://app.example.com/ | head -1
# expect 405 or 501, not 200
```

#### Fix it

nginx

```
if ($request_method !~ ^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$) {
  return 405;
}
```

Apache

```
TraceEnable off
```

### Patches current; no credential or QuickBooks data loggingCode patterns

OS, web server and app server patches must be current. Never log user credentials or QuickBooks data.

Logs are often shipped to third-party services and kept for a long time. A logged refresh token is a working credential to a customer’s books.

#### Fix it

Node

```
// log a fingerprint, never the token
logger.info({ realm: hash(realmId), tokenTail: token.refresh_token.slice(-4) }, 'token refreshed');
```

## Attack vulnerabilities

### Cross-Site Request Forgery protectionCode patterns

State-changing requests (including the OAuth flow) must be protected against CSRF.

Intuit’s sample code passes a fixed \`state\` value. Shipping that means anyone can make a victim’s browser connect the attacker’s QuickBooks company to the victim’s account.

#### Fix it

Node (intuit-oauth)

```
// /connect
req.session.oauthState = crypto.randomBytes(32).toString('hex');
res.redirect(oauthClient.authorizeUri({ scope, state: req.session.oauthState }));

// /callback
if (!req.query.state || req.query.state !== req.session.oauthState) return res.redirect('/error');
delete req.session.oauthState;
```

### Cross-Site Scripting (reflected & stored)Code patterns

Validate input and encode output to prevent reflected and stored XSS.

Customer names, memos and line descriptions come from QuickBooks users and can contain markup. Escape them like any other user input.

### SQL and XML injectionCode patterns

Use parameterized queries and safe XML parsing.

This applies to your database and also to QuickBooks query strings. A customer name like O’Brien breaks an unescaped \`WHERE DisplayName = '…'\` query.

### Authentication, session management, access controlSign-off

Authentication, session management and functional-level access control must be robust.

The most common real-world issue is an insecure direct object reference: changing a realmId or record id in a request shows another customer’s data. Check that ownership is enforced on every route.

### Validated forwards and redirectsAutomated

Redirect logic must be validated so it cannot be abused as an open redirect.

Login and OAuth flows often take a \`next\` or \`returnTo\` parameter. Only allow relative paths or an allow-list of hosts.

## Cookies & sessions

### Secure and HttpOnly on session cookiesAutomated

All session cookies must set the Secure and HttpOnly attributes.

\`Secure\` keeps the cookie off plain HTTP. \`HttpOnly\` keeps it away from JavaScript, so an XSS bug can’t steal the session.

#### Check it yourself

```
curl -sI https://app.example.com/login | grep -i set-cookie
```

#### Fix it

express-session

```
session({ cookie: { secure: true, httpOnly: true, sameSite: 'lax' }, proxy: true, ... })
```

Django

```
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True
```

Rails

```
Rails.application.config.session_store :cookie_store, secure: true, httponly: true, same_site: :lax
```

## OAuth token management

### Tokens never exposed or sharedCode patterns

Intuit OAuth tokens and customer-identifying information must not be exposed in the app or shared with other parties.

Tokens belong on the server. The browser should only ever hold an opaque session cookie, and a disconnect should revoke the tokens with Intuit.

### Refresh token and realmId encrypted at restCode patterns

Refresh token and realmId must be encrypted (AES preferred, or 3DES) and stored in persistent storage.

The refresh token lasts for months and the realmId identifies the company. Both need encrypting before they reach your database or cache.

#### Fix it

Node (AES-256-GCM)

```
import crypto from 'node:crypto';
const key = Buffer.from(process.env.TOKEN_ENCRYPTION_KEY, 'base64'); // 32 bytes

export function encrypt(plain) {
  const iv = crypto.randomBytes(12);
  const c = crypto.createCipheriv('aes-256-gcm', key, iv);
  const data = Buffer.concat([c.update(plain, 'utf8'), c.final()]);
  return Buffer.concat([iv, c.getAuthTag(), data]).toString('base64');
}

export function decrypt(blob) {
  const buf = Buffer.from(blob, 'base64');
  const d = crypto.createDecipheriv('aes-256-gcm', key, buf.subarray(0, 12));
  d.setAuthTag(buf.subarray(12, 28));
  return Buffer.concat([d.update(buf.subarray(28)), d.final()]).toString('utf8');
}
```

Python (cryptography)

```
import os, base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = base64.b64decode(os.environ['TOKEN_ENCRYPTION_KEY'])

def encrypt(plain: str) -> str:
    nonce = os.urandom(12)
    return base64.b64encode(nonce + AESGCM(key).encrypt(nonce, plain.encode(), None)).decode()
```

Rails

```
class QuickbooksConnection < ApplicationRecord
  encrypts :refresh_token
  encrypts :realm_id, deterministic: true
end
```

### Encryption key kept in separate configurationCode patterns

The AES key must be stored in a separate configuration file, not inline in code.

A key committed next to the ciphertext protects nothing. Load it from an environment variable, a config file outside the repo, or a secrets manager, and plan for rotation.

## Token-endpoint redirect rule

### Token-bearing endpoints respond with 302, not HTMLAutomated

Endpoints receiving tokens or sensitive data in URL parameters must not return HTML; they should 302 redirect to avoid Referer leaks.

Intuit sends the user back to your redirect URI with \`code\` and \`realmId\` in the query string. If that URL renders a page with any third-party asset (analytics, fonts, a CDN image), the browser can send the full URL in the Referer header.

#### Check it yourself

```
curl -si "https://app.example.com/oauth/callback?code=x&state=y&realmId=1" | grep -iE "^(HTTP|location|content-type)"
# expect: HTTP/1.1 302 and a location without code/realmId
```

#### Fix it

Express

```
app.get('/oauth/callback', async (req, res) => {
  try {
    await handleCallback(req);          // validate state, exchange code, store encrypted
    res.redirect(302, '/app/connected');
  } catch {
    res.redirect(302, '/app/connect-error'); // errors redirect too
  }
});
```

## QuickBooks data usage

### No third-party access to QuickBooks dataSign-off

The app must not give third parties access to customer QuickBooks data, via APIs or any other means.

This includes indirect sharing: sending invoices to an LLM API, piping payloads into product analytics, or error trackers capturing request bodies. Review every outbound integration that sits next to QuickBooks data.

### Store QuickBooks data only for functional useSign-off

Do not export, save or store QuickBooks data for any purpose other than the functional use of the app.

Keep only what the feature needs, set retention periods, and delete data when a customer disconnects.

## Building on QuickBooks

* [QuickBooks integration](https://www.apideck.com/integrations/quickbooks)

  Data models and coverage for QuickBooks Online through Apideck.

* [Intuit’s annual QuickBooks security review](https://www.apideck.com/blog/quickbooks-security-review)

  Who gets reviewed, what Intuit checks and how to prepare before the vendor emails.

* [How to integrate with QuickBooks Online](https://www.apideck.com/blog/how-to-integrate-your-app-with-quickbooks-online)

  A walkthrough of the QuickBooks Online API and OAuth flow.

* [QuickBooks API pricing and the Intuit App Partner Program](https://www.apideck.com/blog/quickbooks-api-pricing-and-the-intuit-app-partner-program)

  What the partner tiers and API call limits mean for your app.

* [Connect QuickBooks through Apideck](https://developers.apideck.com/apis/accounting/quickbooks/configuration)

  Set up your Intuit app with the Apideck redirect URI.

* [Accounting API](https://www.apideck.com/accounting-api)

  One API for QuickBooks, Xero, NetSuite and more.

* [Vault](https://www.apideck.com/products/vault)

  Hosted OAuth, token storage and refresh for every connection.

Last updated October 28, 2026

This tool is provided as is, without warranty of any kind. It is a self-assessment aid and not legal, compliance or security advice. Results do not guarantee that an app meets Intuit's requirements or will be approved. You are responsible for your app's security and for confirming requirements against [Intuit's official documentation](https://developer.intuit.com/app/developer/qbo/docs/go-live/publish-app/security-requirements). Only scan systems you own or are authorized to test.

Not affiliated with, endorsed by or sponsored by Intuit Inc. Intuit and QuickBooks are trademarks of Intuit Inc., used here only to describe what the tool checks against.
