diff --git a/models/CBWIREController.cfc b/models/CBWIREController.cfc index de873c7d..cb2e8f27 100644 --- a/models/CBWIREController.cfc +++ b/models/CBWIREController.cfc @@ -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"; @@ -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(); } @@ -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 ) { @@ -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 ); arguments._comp.snapshot = deserializeJSON( arguments._comp.snapshot ); return arguments._comp; } ); @@ -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 ) { + 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 ) { @@ -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 ){ @@ -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, "@" ); @@ -243,9 +276,9 @@ component singleton { } } - /** + /** * Returns the path to the modules folder. - * + * * @module string | The name of the module. * * @return string @@ -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(); @@ -296,7 +329,7 @@ component singleton { /** * Returns the ColdBox RequestContext object. - * + * * @return The ColdBox RequestContext object. */ function getEvent(){ @@ -305,7 +338,7 @@ component singleton { /** * Returns any request assets defined by components during the request. - * + * * @return struct */ function getRequestAssets() { @@ -316,7 +349,7 @@ component singleton { /** * Returns the ColdBox ConfigSettings object. - * + * * @return struct */ function getConfigSettings(){ @@ -325,7 +358,7 @@ component singleton { /** * Returns an array of preprocessor instances. - * + * * @return An array of preprocessor instances. */ function getPreprocessors(){ @@ -333,7 +366,7 @@ component singleton { 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", @@ -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; } @@ -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"; } @@ -384,7 +417,7 @@ component singleton { /** * Returns HTML to persist the state of anything inside the call. - * + * * @return string */ function persist( name ) { @@ -393,7 +426,7 @@ component singleton { /** * Ends the persistence of the state of anything inside the call. - * + * * @return string */ function endPersist() { @@ -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) { @@ -419,7 +452,7 @@ component singleton { /** * Generates a CSRF token for the current request. - * + * * @return string */ function generateCSRFToken() { @@ -429,7 +462,7 @@ component singleton { /** * Returns the base URL for incoming requests. - * + * * @return string */ function getBaseURL() { @@ -457,7 +490,7 @@ component singleton { /** * Verifies signed upload URL. - * + * * @return boolean */ function verifySignedUploadURL( expires, signature ) { @@ -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"; } } \ No newline at end of file diff --git a/models/Component.cfc b/models/Component.cfc index ef9c8a36..5b2c4547 100644 --- a/models/Component.cfc +++ b/models/Component.cfc @@ -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. * @@ -1244,7 +1233,7 @@ component output="true" { "errors": [], "locale": "en" ], - "checksum": _generateChecksum() + "checksum": "" }; // Prepend any passed in params into our forMount array @@ -1252,10 +1241,10 @@ component output="true" { 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 @@ -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 ); @@ -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 @@ -1358,7 +1347,7 @@ component output="true" { return [ "data": _getDataProperties(), "memo": _getMemo(), - "checksum": _generateChecksum() + "checksum": "" ]; } @@ -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 diff --git a/test-harness/tests/specs/CBWIRESpec.cfc b/test-harness/tests/specs/CBWIRESpec.cfc index 0e0e4c5c..2f73f876 100644 --- a/test-harness/tests/specs/CBWIRESpec.cfc +++ b/test-harness/tests/specs/CBWIRESpec.cfc @@ -1466,6 +1466,43 @@ component extends="coldbox.system.testing.BaseTestCase" { var result = cbwireController.wire( "should_load_external_modules@ExternalModule" ); expect( result ).toInclude( "External Module Loaded" ); } ); + + it( "can return a valid JSON snapshot string with a valid checksum", function(){ + // get snapshot struct from wires.TestComponent + var snapshot = getInstance("wires.TestComponent") + ._withEvent( getRequestContext( ) ) + ._withPath( "wires.TestComponent" ) + ._getSnapshot(); + // get JSON string of the snapshot with checksum inserted + var snapshotJSON = cbwireController._caclulateChecksum( snapshot ); + // test returned json + expect( isJson(snapshotJSON) ).toBeTrue(); + expect( deserializeJson( snapshotJSON ).keyExists("checksum") ).toBeTrue(); + expect( len( deserializeJson( snapshotJSON ).checksum ) ).toBeTrue(); + // run _validateChecksum on the snapshotJSON to ensure it doesn't throw an error + expect( function() { + cbwireController._validateChecksum( snapshotJSON ) + } ).notToThrow( message="The snapshot JSON had an issue with checksum validation" ); + } ); + + it( "throws error when snapshot is tampered with", function(){ + // get snapshot struct from wires.TestComponent + var snapshot = getInstance("wires.TestComponent") + ._withEvent( getRequestContext( ) ) + ._withPath( "wires.TestComponent" ) + ._getSnapshot(); + // get JSON string of the snapshot with checksum inserted + var snapshotJSON = cbwireController._caclulateChecksum( snapshot ); + // test returned json + expect( isJson(snapshotJSON) ).toBeTrue(); + expect( deserializeJson( snapshotJSON ).keyExists("checksum") ).toBeTrue(); + expect( len( deserializeJson( snapshotJSON ).checksum ) ).toBeGT( 0, "snapshot JSON was returned without a checksum value" ) + // run _validateChecksum with modified snapshotJSON to ensure it doesn't throw an error + expect( function() { + cbwireController._validateChecksum( replace( snapshotJSON, "CBWIRE Rocks!", "CBWIRE Is Awesome!" ) ) + } ).toThrow( message="The snapshot JSON had an issue with checksum validation" ); + } ); + }); describe( "Preprocessors", function() { @@ -1612,7 +1649,8 @@ component extends="coldbox.system.testing.BaseTestCase" { "calls": arguments.calls, "snapshot": { "data": arguments.data, - "memo": arguments.memo + "memo": arguments.memo, + "checksum": "" }, "updates": arguments.updates } @@ -1621,7 +1659,7 @@ component extends="coldbox.system.testing.BaseTestCase" { }; response.content.components = response.content.components.map( function( _comp ) { - _comp.snapshot = serializeJson( _comp.snapshot ); + _comp.snapshot = getInstance( "CBWIREController@cbwire" )._caclulateChecksum( _comp.snapshot ); return _comp; } );