Skip to main content

Security Best Practices

Secure coding practices protect your NetSuite data and prevent unauthorized access or data breaches.


Security Principles

┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY PRINCIPLES │
└─────────────────────────────────────────────────────────────────────────────┘

1. LEAST PRIVILEGE
└── Only request permissions you need

2. DEFENSE IN DEPTH
└── Multiple layers of security

3. INPUT VALIDATION
└── Never trust user input

4. SECURE DEFAULTS
└── Default to most restrictive settings

5. FAIL SECURELY
└── Errors should not expose information

6. AUDIT LOGGING
└── Log security-relevant events

Input Validation

Validate All User Input

/**
* Input validation utilities
*/
const validators = {

isValidEmail: (email) => {
if (!email || typeof email !== 'string') return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
},

isValidId: (id) => {
if (!id) return false;
const numId = parseInt(id);
return !isNaN(numId) && numId > 0;
},

isValidDate: (dateStr) => {
if (!dateStr) return false;
const date = new Date(dateStr);
return date instanceof Date && !isNaN(date);
},

sanitizeString: (str, maxLength = 255) => {
if (!str || typeof str !== 'string') return '';
// Remove potential script tags
return str.replace(/<[^>]*>/g, '')
.substring(0, maxLength)
.trim();
},

sanitizeNumber: (num) => {
const parsed = parseFloat(num);
return isNaN(parsed) ? 0 : parsed;
}
};

RESTlet Input Validation

/**
* Secure RESTlet with input validation
*/
const post = (params) => {
// Validate required parameters
if (!params.customerId) {
return {
success: false,
error: 'Customer ID is required'
};
}

// Validate parameter types
if (!validators.isValidId(params.customerId)) {
return {
success: false,
error: 'Invalid customer ID format'
};
}

// Sanitize string inputs
const memo = validators.sanitizeString(params.memo, 500);
const amount = validators.sanitizeNumber(params.amount);

// Validate business rules
if (amount < 0 || amount > 1000000) {
return {
success: false,
error: 'Amount must be between 0 and 1,000,000'
};
}

try {
// Process with validated data
return processRequest({
customerId: parseInt(params.customerId),
memo,
amount
});
} catch (e) {
log.error('Request Error', e.message);
return {
success: false,
error: 'An error occurred processing your request'
};
}
};

Suitelet Form Validation

/**
* Validate Suitelet POST parameters
*/
const onRequest = (context) => {
if (context.request.method === 'POST') {
const params = context.request.parameters;

// Validate CSRF token (if implemented)
if (!validateCsrfToken(params.token)) {
throw error.create({
name: 'INVALID_TOKEN',
message: 'Invalid or expired form token'
});
}

// Validate required fields
const validation = validateFormData(params);
if (!validation.valid) {
showError(context, validation.message);
return;
}

// Process validated data
processFormSubmission(params);
}
};

Prevent Injection Attacks

SQL Injection Prevention

// GOOD: Use SuiteQL with parameterized queries
const results = query.runSuiteQL({
query: `
SELECT id, companyname
FROM customer
WHERE id = ?
`,
params: [customerId] // Parameters are escaped
});

// AVOID: String concatenation in queries
const results = query.runSuiteQL({
query: `
SELECT id, companyname
FROM customer
WHERE id = ${customerId} // DANGEROUS!
`
});

XSS Prevention

/**
* HTML encoding for output
*/
const encodeHTML = (str) => {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
};

// Usage in Suitelet
const buildPage = (userData) => {
// Encode user-provided data before displaying
return `
<h1>Welcome, ${encodeHTML(userData.name)}</h1>
<p>Email: ${encodeHTML(userData.email)}</p>
`;
};

Script Injection Prevention

// NEVER use eval() with user input
// AVOID:
eval(userProvidedCode); // EXTREMELY DANGEROUS

// AVOID:
new Function(userProvidedCode)(); // ALSO DANGEROUS

// If dynamic behavior needed, use whitelisted options
const ALLOWED_OPERATIONS = {
'add': (a, b) => a + b,
'subtract': (a, b) => a - b,
'multiply': (a, b) => a * b
};

const performOperation = (operation, a, b) => {
if (!ALLOWED_OPERATIONS[operation]) {
throw new Error('Invalid operation');
}
return ALLOWED_OPERATIONS[operation](a, b);
};

Secure Credential Handling

Never Hardcode Credentials

// NEVER do this:
const API_KEY = 'sk_live_abc123456789'; // NEVER!
const PASSWORD = 'mySecretPassword'; // NEVER!

// GOOD: Use script parameters
const getCredentials = () => {
const script = runtime.getCurrentScript();
return {
apiKey: script.getParameter({ name: 'custscript_api_key' }),
secret: script.getParameter({ name: 'custscript_api_secret' })
};
};

// BETTER: Use NetSuite secrets management
const getSecureCredentials = () => {
// Store in custom record with restricted access
const config = search.lookupFields({
type: 'customrecord_api_credentials',
id: 1,
columns: ['custrecord_api_key', 'custrecord_api_secret']
});
return config;
};

OAuth Token Handling

/**
* Secure OAuth implementation
*/
define(['N/https', 'N/runtime'], (https, runtime) => {

const getOAuthHeaders = () => {
const script = runtime.getCurrentScript();

// Get tokens from secure parameters
const consumerKey = script.getParameter({ name: 'custscript_consumer_key' });
const token = script.getParameter({ name: 'custscript_token' });

// Never log tokens
// log.debug('Tokens', { consumerKey, token }); // DON'T DO THIS

return buildOAuthHeader(consumerKey, token);
};

return { getOAuthHeaders };
});

Access Control

Role-Based Access

/**
* Check user role before performing actions
*/
const checkPermission = (requiredRole) => {
const user = runtime.getCurrentUser();
const userRole = user.role;

const ALLOWED_ROLES = {
'admin': [3], // Administrator
'manager': [3, 1001], // Admin + Manager role
'user': [3, 1001, 1002] // Admin + Manager + User role
};

if (!ALLOWED_ROLES[requiredRole].includes(userRole)) {
throw error.create({
name: 'INSUFFICIENT_PERMISSION',
message: 'You do not have permission to perform this action'
});
}
};

// Usage
const deleteRecord = (recordId) => {
checkPermission('admin'); // Only admins can delete

record.delete({
type: record.Type.CUSTOMER,
id: recordId
});
};

Script Deployment Audience

<!-- Restrict who can execute the script -->
<scriptdeployment scriptid="customdeploy_admin_suitelet">
<status>RELEASED</status>
<allroles>F</allroles> <!-- Not all roles -->
<audslctrole>
<role>ADMINISTRATOR</role> <!-- Only Admin -->
<role>[scriptid=customrole_it_manager]</role> <!-- Custom role -->
</audslctrole>
</scriptdeployment>

Record-Level Access

/**
* Verify user can access record
*/
const verifyRecordAccess = (recordType, recordId) => {
try {
// Attempt to load - will fail if no access
const rec = record.load({
type: recordType,
id: recordId
});
return true;
} catch (e) {
if (e.name === 'INSUFFICIENT_PERMISSION') {
log.audit('Access Denied', {
user: runtime.getCurrentUser().id,
recordType,
recordId
});
return false;
}
throw e;
}
};

Secure External Communications

HTTPS Only

/**
* Always use HTTPS for external calls
*/
const callExternalApi = (endpoint, data) => {
// Ensure HTTPS
if (!endpoint.startsWith('https://')) {
throw new Error('HTTPS required for external calls');
}

return https.post({
url: endpoint,
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`
}
});
};

Certificate Validation

/**
* Don't disable certificate validation
*/
// NEVER do this in production:
// https.post({ ..., certificateVerification: false });

// ALWAYS validate certificates:
const response = https.post({
url: 'https://api.example.com/data',
body: data,
headers: headers
// certificateVerification defaults to true
});

Secure Headers

/**
* Set security headers in Suitelet responses
*/
const onRequest = (context) => {
// Add security headers
const response = context.response;

response.setHeader({
name: 'X-Content-Type-Options',
value: 'nosniff'
});

response.setHeader({
name: 'X-Frame-Options',
value: 'SAMEORIGIN'
});

response.setHeader({
name: 'X-XSS-Protection',
value: '1; mode=block'
});

// Write response
response.write(buildPage());
};

Audit Logging

Log Security Events

/**
* Security event logging
*/
const logSecurityEvent = (eventType, details) => {
const user = runtime.getCurrentUser();

log.audit('SECURITY_EVENT', JSON.stringify({
eventType,
userId: user.id,
userEmail: user.email,
role: user.role,
timestamp: new Date().toISOString(),
...details
}));
};

// Usage
const performSensitiveAction = (data) => {
logSecurityEvent('SENSITIVE_ACTION', {
action: 'delete_customer',
customerId: data.customerId
});

// Perform action...
};

const handleLoginAttempt = (success, email) => {
logSecurityEvent(success ? 'LOGIN_SUCCESS' : 'LOGIN_FAILURE', {
email,
ipAddress: 'N/A' // NetSuite doesn't expose this easily
});
};

Audit Trail for Data Changes

/**
* Create audit trail for sensitive changes
*/
const createAuditRecord = (action, recordType, recordId, oldValues, newValues) => {
const auditRecord = record.create({
type: 'customrecord_audit_trail'
});

auditRecord.setValue({
fieldId: 'custrecord_at_action',
value: action
});

auditRecord.setValue({
fieldId: 'custrecord_at_record_type',
value: recordType
});

auditRecord.setValue({
fieldId: 'custrecord_at_record_id',
value: recordId
});

auditRecord.setValue({
fieldId: 'custrecord_at_user',
value: runtime.getCurrentUser().id
});

auditRecord.setValue({
fieldId: 'custrecord_at_old_values',
value: JSON.stringify(oldValues)
});

auditRecord.setValue({
fieldId: 'custrecord_at_new_values',
value: JSON.stringify(newValues)
});

auditRecord.save();
};

Error Message Security

Don't Expose System Details

// AVOID: Exposing internal details
try {
processData();
} catch (e) {
throw new Error(`Database error: ${e.message} at line ${e.lineNumber}`);
}

// GOOD: Generic user message, detailed logging
try {
processData();
} catch (e) {
// Log full details internally
log.error('Process Error', {
message: e.message,
stack: e.stack,
line: e.lineNumber
});

// Return generic message to user
throw error.create({
name: 'PROCESSING_ERROR',
message: 'An error occurred. Please contact support if this persists.',
notifyOff: true
});
}

Hide Stack Traces

/**
* Secure error response for RESTlet
*/
const get = (params) => {
try {
return processRequest(params);
} catch (e) {
// Log full error internally
log.error('API Error', {
params,
error: e.message,
stack: e.stack
});

// Return sanitized error to client
return {
success: false,
error: {
code: getErrorCode(e),
message: getPublicMessage(e)
// NO stack trace, NO internal details
}
};
}
};

const getErrorCode = (e) => {
const errorCodes = {
'RCRD_DSNT_EXIST': 'NOT_FOUND',
'INSUFFICIENT_PERMISSION': 'FORBIDDEN',
'SSS_MISSING_REQD_ARGUMENT': 'BAD_REQUEST'
};
return errorCodes[e.name] || 'INTERNAL_ERROR';
};

const getPublicMessage = (e) => {
const publicMessages = {
'RCRD_DSNT_EXIST': 'The requested record was not found',
'INSUFFICIENT_PERMISSION': 'You do not have permission for this action',
'SSS_MISSING_REQD_ARGUMENT': 'Required parameters are missing'
};
return publicMessages[e.name] || 'An unexpected error occurred';
};

Secure File Operations

Validate File Types

/**
* File upload validation
*/
const ALLOWED_EXTENSIONS = ['csv', 'xlsx', 'pdf', 'jpg', 'png'];
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

const validateFileUpload = (fileObj) => {
// Check file exists
if (!fileObj) {
return { valid: false, error: 'No file provided' };
}

// Check extension
const name = fileObj.name;
const extension = name.split('.').pop().toLowerCase();

if (!ALLOWED_EXTENSIONS.includes(extension)) {
return {
valid: false,
error: `File type .${extension} not allowed`
};
}

// Check size
if (fileObj.size > MAX_FILE_SIZE) {
return {
valid: false,
error: 'File exceeds maximum size of 10MB'
};
}

return { valid: true };
};

Secure File Paths

/**
* Prevent path traversal attacks
*/
const sanitizePath = (userPath) => {
// Remove path traversal attempts
const sanitized = userPath
.replace(/\.\./g, '') // Remove ..
.replace(/\/\//g, '/') // Remove //
.replace(/\\/g, '/') // Normalize slashes
.replace(/^\//, ''); // Remove leading /

// Ensure path is within allowed directory
const ALLOWED_ROOT = '/SuiteScripts/uploads/';

if (!sanitized.startsWith('uploads/')) {
throw new Error('Invalid file path');
}

return ALLOWED_ROOT + sanitized.substring(8);
};

Security Checklist

┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY CHECKLIST │
└─────────────────────────────────────────────────────────────────────────────┘

INPUT VALIDATION
☐ All user inputs validated
☐ Input types verified
☐ String lengths limited
☐ Special characters handled

INJECTION PREVENTION
☐ Parameterized queries used
☐ HTML output encoded
☐ No eval() with user data
☐ No dynamic code execution

AUTHENTICATION & AUTHORIZATION
☐ Role checks implemented
☐ Script deployment audience set
☐ Record access verified
☐ Sensitive actions logged

CREDENTIALS
☐ No hardcoded secrets
☐ Tokens stored securely
☐ Credentials never logged
☐ API keys in parameters

COMMUNICATIONS
☐ HTTPS enforced
☐ Certificates validated
☐ Security headers set
☐ Sensitive data encrypted

ERROR HANDLING
☐ Generic user messages
☐ Detailed internal logging
☐ No stack traces exposed
☐ System details hidden

Next Steps