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
101 changes: 67 additions & 34 deletions models/CBWIREController.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ component singleton {

// Inject module settings
property name="moduleSettings" inject="coldbox:modulesettings:cbwire";

// Inject module service
property name="moduleService" inject="coldbox:moduleService";

Expand Down Expand Up @@ -43,8 +43,8 @@ component singleton {
._withKey( arguments.key );

// If the component is lazy loaded, we need to generate an x-intersect snapshot of the component
return arguments.lazy ?
local.instance._generateXIntersectLazyLoadSnapshot( params=arguments.params ) :
return arguments.lazy ?
local.instance._generateXIntersectLazyLoadSnapshot( params=arguments.params ) :
local.instance._render();
}

Expand All @@ -53,7 +53,7 @@ component singleton {
*
* @incomingRequest The JSON struct payload of the incoming request.
* @event The event object.
*
*
* @return A struct representing the response with updated component details or an error message.
*/
function handleRequest( incomingRequest, event ) {
Expand All @@ -73,6 +73,7 @@ component singleton {
}
// Perform additional deserialization of the component snapshots
local.payload.components = local.payload.components.map( function( _comp ) {
_validateChecksum( arguments._comp.snapshot );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to change this up a bit. You are passing in the snapshot as a string into _validateChecksum but we are already deserializing it below this line into an object. I would move this to after the deserialization call so that we don't have to use regex methods ( which are slow ) for parsing the checksum.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I was too caught up with the issue of unordered structs in CF as opposed to JS and PHP, thus causing potentially different JSON strings after serializeJSON/deserializeJSON that I didn't even think about deserializing to get checksum value, doing a replace on the original payload string to remove the checksum and then verifying... I will get that fixed up shortly.

arguments._comp.snapshot = deserializeJSON( arguments._comp.snapshot );
return arguments._comp;
} );
Expand Down Expand Up @@ -118,13 +119,45 @@ component singleton {
return local.componentsResult;
}

/**
* Calculates a checksum for the component's payload, inserts the checksum into the payload,
* and returns the updated payload as a JSON string.
*
* @payload struct | the payload to calculate the checksum for
*
* @return string
*/
function _caclulateChecksum( snapshot ) {
var secret = moduleSettings.keyExists("secret") ? moduleSettings.secret : hash( moduleSettings.moduleRootPath );
var serializedSnapshot = serializeJson( arguments.snapshot );
var checksum = hmac( serializedSnapshot, secret, "HMACSHA256");
return replace( serializedSnapshot, '"checksum":""', '"checksum":"#checksum#"', "all" )
}

/**
* Validates checksum for the component's data from snapshot.
*
* @payload string | the JSON string of the component snapshot as posted by livewire
*
* @return void
*/
function _validateChecksum( snapshot ) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change method to accept snapshot as a struct so we can avoid regex parsing

Copy link
Contributor Author

@mrigsby mrigsby Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Livewire passes the payload as a JSON string. The crux of the issue is that we can't use deserialize the payload, remove the checksum key, then serialize the struct and expect to get the same JSON string that was originally used to generate the checksum like livewire does. PHP and JS both keep struct/object ordering, but CFML doesn't and deserializeJSON() doesn't have an option to produce an ordered struct.

I can avoid the regex completely by deserializing the payload, getting the checksum value, using replace on the original JSON string payload to clear out the value of the checksum, then verifying the checksum.

Note: I changed the payload to always include and empty string for the checksum before generating the checksum.

I hope I am clearly explaining the issue. I will try to get this all cleaned up in the next little bit so you can take a look at the changes.

if( !isJson( snapshot ) ) throw( type="CBWIRECorruptPayloadException", message="Payload is not valid JSON." );
var deserializedSnapshot = deserializeJSON( arguments.snapshot );
if( !deserializedSnapshot.keyExists("checksum") ) throw( type="CBWIRECorruptPayloadException", message="Checksum Not Found." );
var secret = moduleSettings.keyExists("secret") ? moduleSettings.secret : hash( moduleSettings.moduleRootPath );
if( deserializedSnapshot.checksum != hmac( replace( snapshot, deserializedSnapshot.checksum, "", "one" ), secret, "HMACSHA256") ){
throw( type="CBWIRECorruptPayloadException", message="Checksum Mismatch." );
}
}

/**
* Uploads all files from the request to the specified destination
* after verifying the signed URL.
*
*
* @incomingRequest The JSON struct payload of the incoming request.
* @event The event object.
*
*
* @return A struct representing the response with updated component details or an error message.
*/
function handleFileUpload( incomingRequest, event ) {
Expand Down Expand Up @@ -158,10 +191,10 @@ component singleton {

/**
* Handles the preview of a file by reading the file metadata and sending it back to the client.
*
*
* @incomingRequest The JSON struct payload of the incoming request.
* @event The event object.
*
*
* @return file contents
*/
function handleFilePreview( incomingRequest, event ){
Expand Down Expand Up @@ -191,18 +224,18 @@ component singleton {
* @componentName The name of the component to instantiate, possibly including a namespace.
* @params Optional parameters to pass to the component constructor.
* @key Optional key to use when retrieving the component from WireBox.
*
*
* @return The instantiated component object.
* @throws ApplicationException If the component cannot be found or instantiated.
*/
function createInstance( name ) {
// Determine if the component name traverses a valid namespace or directory structure
local.fullComponentPath = arguments.name;

if ( !local.fullComponentPath contains "wires." ) {
local.fullComponentPath = "wires." & local.fullComponentPath;
}

if ( find( "@", local.fullComponentPath ) ) {
// This is a module reference, find in our module
var params = listToArray( local.fullComponentPath, "@" );
Expand Down Expand Up @@ -243,9 +276,9 @@ component singleton {
}
}

/**
/**
* Returns the path to the modules folder.
*
*
* @module string | The name of the module.
*
* @return string
Expand Down Expand Up @@ -286,8 +319,8 @@ component singleton {
* Returns the path to the wires folder within a module path.
*
* @module string | The name of the module.
*
* @return string
*
* @return string
*/
function getModuleWiresPath( module ) {
local.moduleRegistry = moduleService.getModuleRegistry();
Expand All @@ -296,7 +329,7 @@ component singleton {

/**
* Returns the ColdBox RequestContext object.
*
*
* @return The ColdBox RequestContext object.
*/
function getEvent(){
Expand All @@ -305,7 +338,7 @@ component singleton {

/**
* Returns any request assets defined by components during the request.
*
*
* @return struct
*/
function getRequestAssets() {
Expand All @@ -316,7 +349,7 @@ component singleton {

/**
* Returns the ColdBox ConfigSettings object.
*
*
* @return struct
*/
function getConfigSettings(){
Expand All @@ -325,15 +358,15 @@ component singleton {

/**
* Returns an array of preprocessor instances.
*
*
* @return An array of preprocessor instances.
*/
function getPreprocessors(){
// Check if we've already scanned the folder
if( structKeyExists( variables, "preprocessors" ) ){
return variables.preprocessors;
}
// List of preprocesssors here. Had to hard code instead of using
// List of preprocesssors here. Had to hard code instead of using
// directoryList because of filesystem differences in various OSes
local.files = [
"TemplatePreprocessor.cfc",
Expand All @@ -351,18 +384,18 @@ component singleton {

/**
* Returns CSS styling needed by Livewire.
*
*
* @return string
*/
function getStyles( cache=true ) {
if (structKeyExists(variables, "styles") && arguments.cache ) {
return variables.styles;
}

savecontent variable="local.html" {
include "styles.cfm";
}

variables.styles = local.html;
return variables.styles;
}
Expand All @@ -372,10 +405,10 @@ component singleton {
* We don't cache the results like we do with
* styles because we need to generate a unique
* CSRF token for each request.
*
*
* @return string
*/
function getScripts() {
function getScripts() {
savecontent variable="local.html" {
include "scripts.cfm";
}
Expand All @@ -384,7 +417,7 @@ component singleton {

/**
* Returns HTML to persist the state of anything inside the call.
*
*
* @return string
*/
function persist( name ) {
Expand All @@ -393,7 +426,7 @@ component singleton {

/**
* Ends the persistence of the state of anything inside the call.
*
*
* @return string
*/
function endPersist() {
Expand All @@ -402,10 +435,10 @@ component singleton {

/**
* Generates a secure signature for the upload URL.
*
*
* @baseURL string | The base URL for the upload request.
* @expires string | The expiration time for the request.
*
*
* @return string
*/
function generateSignature(baseUrl, expires) {
Expand All @@ -419,7 +452,7 @@ component singleton {

/**
* Generates a CSRF token for the current request.
*
*
* @return string
*/
function generateCSRFToken() {
Expand All @@ -429,7 +462,7 @@ component singleton {

/**
* Returns the base URL for incoming requests.
*
*
* @return string
*/
function getBaseURL() {
Expand Down Expand Up @@ -457,7 +490,7 @@ component singleton {

/**
* Verifies signed upload URL.
*
*
* @return boolean
*/
function verifySignedUploadURL( expires, signature ) {
Expand Down Expand Up @@ -523,11 +556,11 @@ component singleton {

/**
* Returns the URI endpoint for updating CBWIRE components.
*
*
* @return string
*/
function getUpdateEndpoint() {
var settings = variables.moduleSettings;
var settings = variables.moduleSettings;
return settings.keyExists( "updateEndpoint") && settings.updateEndpoint.len() ? settings.updateEndpoint : "/cbwire/update";
}
}
28 changes: 9 additions & 19 deletions models/Component.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1003,17 +1003,6 @@ component output="true" {
throw( type="CBWIREException", message="The method '#arguments.missingMethodName#' does not exist." );
}

/**
* Generates a checksum for securing the component's data.
*
* @return String The generated checksum.
*/
function _generateChecksum() {
return "f9f66fa895026e389a10ce006daf3f59afaec8db50cdb60f152af599b32f9192";
var secretKey = "YourSecretKey"; // This key should be securely retrieved
return hash(serializeJson(arguments.snapshot) & secretKey, "SHA-256");
}

/**
* Encodes a given string for safe usage within an HTML attribute.
*
Expand Down Expand Up @@ -1244,18 +1233,18 @@ component output="true" {
"errors": [],
"locale": "en"
],
"checksum": _generateChecksum()
"checksum": ""
};

// Prepend any passed in params into our forMount array
arguments.params.each( function( key, value ) {
snapshot.data.forMount.prepend( { "#arguments.key#": arguments.value } );
} );

// Serialize the snapshot to JSON and then encode it for HTML attribute inclusion
local.lazyLoadSnapshot = serializeJson( local.snapshot );
// Serialize the snapshot to JSON, calculate the checksum, and then encode it for HTML attribute inclusion
local.lazyLoadSnapshot = _CBWIREController._caclulateChecksum( local.snapshot )

// Generate the base64 encoded version of the serialized snapshot for use in x-intersect
// Generate the base64 encoded version of the serialized snapshot for use in x-intersect
local.base64EncodedSnapshot = toBase64( local.lazyLoadSnapshot );

// Get our placeholder html
Expand All @@ -1267,7 +1256,7 @@ component output="true" {
}

// Define the wire attributes to append
local.wireAttributes = 'wire:snapshot="' & _encodeAttribute( serializeJson( _getSnapshot() ) ) & '" wire:effects="#_generateWireEffectsAttribute()#" wire:id="#variables._id#"' & ' x-intersect="$wire._lazyMount(&##039;' & local.base64EncodedSnapshot & '&##039;)"';
local.wireAttributes = 'wire:snapshot="' & _encodeAttribute( _CBWIREController._caclulateChecksum( _getSnapshot() ) ) & '" wire:effects="#_generateWireEffectsAttribute()#" wire:id="#variables._id#"' & ' x-intersect="$wire._lazyMount(&##039;' & local.base64EncodedSnapshot & '&##039;)"';

// Determine our outer element
local.outerElement = _getOuterElement( local.html );
Expand Down Expand Up @@ -1318,7 +1307,7 @@ component output="true" {

// Return the HTML response
local.response = [
"snapshot": serializeJson( local.snapshot ),
"snapshot": _CBWIREController._caclulateChecksum( local.snapshot ),
"effects": {
"returns": variables._returnValues,
"html": local.html
Expand Down Expand Up @@ -1358,7 +1347,7 @@ component output="true" {
return [
"data": _getDataProperties(),
"memo": _getMemo(),
"checksum": _generateChecksum()
"checksum": ""
];
}

Expand Down Expand Up @@ -1685,7 +1674,8 @@ component output="true" {
// If this is the initial load, encode the snapshot and insert Livewire attributes
if ( variables._initialLoad ) {
// Encode the snapshot for HTML attribute inclusion and process the view content
local.snapshotEncoded = _encodeAttribute( serializeJson( _getSnapshot() ) );
// local.snapshotEncoded = _encodeAttribute( serializeJson( _getSnapshot() ) );
local.snapshotEncoded = _encodeAttribute( _CBWIREController._caclulateChecksum( _getSnapshot() ) );
return _insertInitialLivewireAttributes( local.trimmedHTML, local.snapshotEncoded, variables._id );
} else {
// Return the trimmed HTML content
Expand Down
Loading
Loading