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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
// 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
- Code Standards - Coding conventions
- Error Handling - Error management
- Performance - Optimization techniques