Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(box testbox run:*)",
"WebSearch",
"WebFetch(domain:github.com)"
],
"deny": [],
"ask": []
}
}
22 changes: 20 additions & 2 deletions ModuleConfig.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ component {
this.version = "@build.version@+@build.number@";
this.author = "Ortus Solutions";
this.webUrl = "https://github.com/coldbox-modules/cbwire";
this.dependencies = [];
this.dependencies = [ "cbstorages" ];
this.entryPoint = "cbwire";
this.layoutParentLookup = false;
this.viewParentLookup = false;
Expand Down Expand Up @@ -66,7 +66,25 @@ component {
* We recommend always leaving this enabled, but you can disable it
* as needed.
*/
"checksumValidation": true
"checksumValidation": true,
/**
* Enables Cross-Site Request Forgery (CSRF) protection for CBWIRE requests.
* When enabled, all component actions require a valid CSRF token.
* When disabled, checksum validation still provides security against tampering.
*/
"csrfEnabled": true,
/**
* Specifies the WireBox mapping for the CSRF token storage service.
* The service must implement the ICSRFStorage interface.
*
* Built-in options:
* - "SessionCSRFStorage@cbwire" (default) - Session-based storage, OWASP recommended
* - "CacheCSRFStorage@cbwire" - Cache-based storage for distributed/clustered systems
*
* You can also provide your own custom implementation that implements
* cbwire.models.interfaces.ICSRFStorage
*/
"csrfService": "SessionCSRFStorage@cbwire"
};

routes = [
Expand Down
4 changes: 2 additions & 2 deletions box.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
],
"contributors":[],
"dependencies":{
"cbcsrf":"^3.1.0+16"
"cbstorages":"^3.0.0"
},
"devDependencies":{
"commandbox-cfformat":"*",
Expand All @@ -45,6 +45,6 @@
"runner":"http://localhost:60299/tests/runner.cfm"
},
"installPaths":{
"cbcsrf":"modules/cbcsrf/"
"cbstorages":"modules/cbstorages/"
}
}
25 changes: 14 additions & 11 deletions models/CBWIREController.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ component accessors="true" singleton {
// Injected RequestService so that we can access the current ColdBox RequestContext.
property name="requestService" inject="coldbox:requestService";

// Inject CBCSRF for CSRF token generation and verification
property name="cbcsrf" inject="provider:@cbcsrf";
// Inject TokenService for CSRF token generation and verification
property name="tokenService" inject="provider:TokenService@cbwire";

// Inject module settings
property name="moduleSettings" inject="coldbox:modulesettings:cbwire";
Expand Down Expand Up @@ -84,13 +84,13 @@ component accessors="true" singleton {
}
);

// Set the CSRF token for the request
local.csrfToken = local.payload._token;
// Validate the CSRF token
local.csrfTokenVerified = variables.wirebox.getInstance( dsl="@cbcsrf" ).verify( local.csrfToken );
// Check the CSRF token, throw 403 if invalid
if( !local.csrfTokenVerified ){
throw( type="CBWIREException", message="Page expired." );
// Verify CSRF token if CSRF protection is enabled
if ( variables.moduleSettings.csrfEnabled ) {
local.csrfToken = local.payload._token;
local.csrfTokenVerified = variables.tokenService.verify( local.csrfToken );
if( !local.csrfTokenVerified ){
throw( type="CBWIREException", message="Page expired." );
}
}

// Perform additional deserialization of the component snapshots
Expand Down Expand Up @@ -530,12 +530,15 @@ component accessors="true" singleton {

/**
* Generates a CSRF token for the current request.
* Returns empty string if CSRF protection is disabled.
*
* @return string
*/
function generateCSRFToken() {
// Generate the CSRF token using the cbcsrf library
return variables.cbcsrf.generate();
// Generate the CSRF token using cbwire's token service if enabled
return variables.moduleSettings.csrfEnabled ?
variables.tokenService.generate() :
"";
}

/**
Expand Down
49 changes: 49 additions & 0 deletions models/interfaces/ICSRFStorage.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Interface for CSRF token storage implementations in cbwire.
*
* Implementations must provide thread-safe storage and retrieval
* of CSRF tokens with session-lifetime persistence.
*
* @author Ortus Solutions
*/
interface {

/**
* Stores a CSRF token for the current session/user context.
*
* @token The CSRF token to store
*
* @return The storage implementation instance (for method chaining)
*/
any function set( required string token );

/**
* Retrieves the stored CSRF token for the current session/user context.
*
* @return The stored token, or empty string if no token exists
*/
string function get();

/**
* Checks if a CSRF token exists in storage for the current session/user context.
*
* @return True if a token exists, false otherwise
*/
boolean function exists();

/**
* Removes the CSRF token from storage (used for rotation/logout).
*
* @return The storage implementation instance (for method chaining)
*/
any function delete();

/**
* Clears all CSRF-related data from storage.
* Useful for cleanup or testing scenarios.
*
* @return The storage implementation instance (for method chaining)
*/
any function clear();

}
118 changes: 118 additions & 0 deletions models/services/TokenService.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* CBWIRE-specific token service for preventing CSRF attacks
* without interfering with cbcsrf module settings.
*
* Delegates storage operations to a configurable ICSRFStorage implementation.
*
* @author Ortus Solutions
*/
component accessors="true" singleton {

property name="moduleSettings" inject="coldbox:modulesettings:cbwire";
property name="wirebox" inject="wirebox";

/**
* Lazy-loaded CSRF storage service
*/
variables.csrfService = "";

/**
* Cache for application metadata to avoid repeated lookups
*/
variables.appMetadata = {};

/**
* Generates a CBWIRE-specific token that doesn't expire.
* Stored using the configured storage implementation and lasts for session lifetime.
*
* @return The generated or existing token
*/
function generate() {
// If we don't have a token yet, generate one
if ( !getCSRFService().exists() ) {
var newToken = generateNewToken();
getCSRFService().set( newToken );
}

return getCSRFService().get();
}

/**
* Verifies a CBWIRE token
*
* @token The token to verify
*
* @return True if valid, false otherwise
*/
function verify( required string token ) {
// Verify token exists and matches
return getCSRFService().exists() && getCSRFService().get() == arguments.token;
}

/**
* Rotates the token (for logout, etc)
*
* @return TokenService instance for chaining
*/
function rotate() {
getCSRFService().delete();
return this;
}

// Private methods

/**
* Lazy-loads the CSRF storage service based on module settings
*
* @return The configured ICSRFStorage implementation
*/
private function getCSRFService() {
if ( !isObject( variables.csrfService ) ) {
variables.csrfService = wirebox.getInstance( moduleSettings.csrfService );
}
return variables.csrfService;
}

private function generateNewToken() {
// Generate a cryptographically secure random token
var tokenBase = "#createUUID()##getRealIP()##randRange( 0, 65535, "SHA1PRNG" )##getTickCount()#";

// Include session ID if sessions are enabled (cross-platform check)
var sessionId = "";
if ( isSessionManagementEnabled() ) {
try {
sessionId = session.sessionid;
} catch ( any e ) {
// Handle cases where session scope exists but sessionid property is not yet available,
// or when session operations fail during application startup
}
}

return uCase( left( hash( tokenBase & sessionId, "SHA-256" ), 40 ) );
}

/**
* Checks if session management is enabled in the application (cross-platform)
*
* @return True if session management is enabled, false otherwise
*/
private function isSessionManagementEnabled() {
if ( structIsEmpty( variables.appMetadata ) ) {
variables.appMetadata = getApplicationMetadata();
}
return structKeyExists( variables.appMetadata, "sessionManagement" ) && variables.appMetadata.sessionManagement;
}

private function getRealIP() {
var headers = getHTTPRequestData().headers;

if ( structKeyExists( headers, "x-cluster-client-ip" ) ) {
return headers[ "x-cluster-client-ip" ];
}
if ( structKeyExists( headers, "X-Forwarded-For" ) ) {
return headers[ "X-Forwarded-For" ];
}

return len( CGI.REMOTE_ADDR ) ? CGI.REMOTE_ADDR : "127.0.0.1";
}
}
70 changes: 70 additions & 0 deletions models/services/csrf/CacheCSRFStorage.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Cache-based CSRF token storage using cbstorages cacheStorage.
* Suitable for distributed/clustered environments where session replication
* is not available or desired.
*
* Tokens are stored in the configured CacheBox cache provider.
* Does not require session management to be enabled - uses cookie/URL fallback.
*
* @author Ortus Solutions
*/
component
accessors="true"
singleton
implements="cbwire.models.interfaces.ICSRFStorage"
{

property name="cacheStorage" inject="cacheStorage@cbstorages";

/**
* The storage key used for cbwire tokens
*/
variables.STORAGE_KEY = "_cbwire_token";

/**
* Stores a CSRF token in cache storage
*
* @token The CSRF token to store
*/
function set( required string token ) {
cacheStorage.set(
variables.STORAGE_KEY,
{ "token": arguments.token, "createdAt": now() }
);
return this;
}

/**
* Retrieves the stored CSRF token from cache
*
* @return The stored token, or empty string if none exists
*/
string function get() {
var data = cacheStorage.get( variables.STORAGE_KEY, {} );
return data.keyExists( "token" ) ? data.token : "";
}

/**
* Checks if a token exists in cache storage
*/
boolean function exists() {
var data = cacheStorage.get( variables.STORAGE_KEY, {} );
return data.keyExists( "token" ) && len( data.token ) > 0;
}

/**
* Removes the token from cache storage
*/
function delete() {
cacheStorage.delete( variables.STORAGE_KEY );
return this;
}

/**
* Clears all CSRF data (same as delete for cache storage)
*/
function clear() {
return delete();
}

}
Loading