REST Endpoints and API Security
Secure ColdFusion REST APIs with authentication, validation, and rate limiting
Secure ColdFusion REST APIs and web services with proper authentication, input validation, rate limiting, and structured logging for production deployments.
ColdFusion REST Services
ColdFusion provides built-in REST support through CFCs with remote access enabled:
// UserAPI.cfc
component restpath="users" rest="true" {
remote any function getUser(required numeric id) httpmethod="GET" {
// Input validation
if (arguments.id < 1) {
restSetResponse({
status: 400,
content: {"error": "Invalid user ID"}
});
return;
}
// Business logic
var user = getUserFromDB(arguments.id);
if (isNull(user)) {
restSetResponse({status: 404});
return {"error": "User not found"};
}
return user;
}
remote any function createUser(required struct userData)
httpmethod="POST"
consumes="application/json"
produces="application/json" {
// Validate input
if (!structKeyExists(arguments.userData, "email")) {
restSetResponse({status: 400});
return {"error": "Email required"};
}
// Create user
var newUser = saveUser(arguments.userData);
restSetResponse({status: 201});
return newUser;
}
}Authentication and Authorization
Token-Based Authentication
// Validate JWT token in Application.cfc
function onRequestStart(targetPage) {
if (findNoCase("/api/", targetPage)) {
var authHeader = getHTTPRequestData().headers["Authorization"] ?: "";
if (!len(authHeader) || !authHeader.startsWith("Bearer ")) {
restSetResponse({status: 401});
writeOutput(serializeJSON({"error": "Unauthorized"}));
return false;
}
var token = listLast(authHeader, " ");
try {
var payload = verifyJWT(token);
request.user = payload;
} catch (any e) {
restSetResponse({status: 401});
writeOutput(serializeJSON({"error": "Invalid token"}));
return false;
}
}
return true;
}API Keys
// Simple API key validation
function validateAPIKey(required string apiKey) {
// Check against database or cache
var validKey = cacheGet("api_key_#arguments.apiKey#");
if (isNull(validKey)) {
var qKey = queryExecute(
"SELECT id, rate_limit FROM api_keys WHERE key_hash = :hash AND active = 1",
{hash: hash(arguments.apiKey, "SHA-256")},
{datasource: "mydb"}
);
if (qKey.recordCount == 0) {
return false;
}
cachePut("api_key_#arguments.apiKey#", qKey, createTimeSpan(0, 1, 0, 0));
validKey = qKey;
}
request.apiKeyData = validKey;
return true;
}Input Validation
- Validate all inputs: Type, format, length, range
- Whitelist approach: Accept only known-good values
- SQL injection: Always use cfqueryparam
- XSS prevention: Encode output appropriately
- Schema validation: Validate JSON/XML against schema
// Input validation example
function validateUserInput(required struct data) {
var errors = [];
// Email validation
if (!structKeyExists(data, "email") ||
!reFindNoCase("^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,}$", data.email)) {
arrayAppend(errors, "Invalid email format");
}
// Age validation
if (structKeyExists(data, "age") &&
(!isNumeric(data.age) || data.age < 0 || data.age > 150)) {
arrayAppend(errors, "Invalid age");
}
return errors;
}Rate Limiting
You should implement rate limiting to prevent API abuse and protect your server resources:
// Token bucket rate limiting
function checkRateLimit(required string identifier, numeric limit=100, numeric window=60) {
var cacheKey = "rate_limit_#arguments.identifier#";
var bucket = cacheGet(cacheKey);
if (isNull(bucket)) {
bucket = {
tokens: arguments.limit,
lastRefill: now()
};
} else {
// Refill tokens based on time elapsed
var elapsed = dateDiff("s", bucket.lastRefill, now());
var tokensToAdd = int(elapsed * (arguments.limit / arguments.window));
bucket.tokens = min(bucket.tokens + tokensToAdd, arguments.limit);
bucket.lastRefill = now();
}
if (bucket.tokens < 1) {
// Rate limit exceeded
restSetResponse({
status: 429,
headers: {"Retry-After": arguments.window}
});
return false;
}
bucket.tokens--;
cachePut(cacheKey, bucket, createTimeSpan(0, 0, arguments.window * 2, 0));
return true;
}Structured Logging with Request IDs
// Application.cfc
function onRequestStart(targetPage) {
// Generate request ID for tracing
request.requestID = createUUID();
// Extract correlation ID from headers if present
var headers = getHTTPRequestData().headers;
request.correlationID = headers["X-Correlation-ID"] ?: request.requestID;
// Log request
writeLog(
file="api_requests",
type="information",
text=serializeJSON({
requestID: request.requestID,
correlationID: request.correlationID,
method: cgi.request_method,
path: cgi.path_info,
userAgent: cgi.http_user_agent,
ip: cgi.remote_addr
})
);
}
function onRequestEnd() {
// Log response
writeLog(
file="api_requests",
type="information",
text=serializeJSON({
requestID: request.requestID,
duration: getTickCount() - request.startTime,
status: getPageContext().getResponse().getStatus()
})
);
}CORS Configuration
// Configure CORS headers
function setCORSHeaders(allowedOrigin="*") {
var response = getPageContext().getResponse();
response.setHeader("Access-Control-Allow-Origin", arguments.allowedOrigin);
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
response.setHeader("Access-Control-Max-Age", "3600");
// Handle preflight OPTIONS requests
if (cgi.request_method == "OPTIONS") {
restSetResponse({status: 204});
abort;
}
}Legacy SOAP Services
Recommendation: Disable legacy SOAP services unless specifically required. Remove the SOAP cfpm package if not in use.
# Remove SOAP package if not needed cfpm remove soap
API Documentation and Versioning
- Include version numbers (v1, v2) in your API URL paths to support multiple versions
- Document all endpoints using OpenAPI/Swagger specifications
- Maintain backward compatibility whenever possible to avoid breaking existing clients
- Provide clear migration guides when you must introduce breaking changes
API Security Checklist
- Authentication implemented (tokens or API keys)
- All inputs validated and sanitized
- Rate limiting configured
- Request IDs logged for tracing
- CORS properly configured
- HTTPS only (no HTTP)
- Error messages don't leak sensitive info
- Legacy SOAP disabled if not needed
- API versioning strategy in place
Gotchas
- Missing authentication allows public access to API endpoints
- Insufficient input validation enables injection attacks
- No rate limiting allows abuse and DDoS attacks
- Verbose error messages leak implementation details
- CORS wildcard (*) allows any origin - use specific domains
- Missing request IDs makes debugging distributed systems difficult