Home>REST and API Hardening

REST Endpoints and API Security

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

Need Help?

Convective can help design and secure REST APIs in ColdFusion. Find out more.