'Ctrl+S' doesn't save modified textures ==> lost hours of work
You can use application handlers, specifically save_pre handler. When you're saving the file, the handler kicks in and allows to execute whatever you want before saving the file.
The following demo saves all modified images when you are hitting save, automatically:
import bpy
from bpy.app.handlers import persistent

@persistent
def save_mod_images(dummy):
    &quot;&quot;&quot; Save all modified images &quot;&quot;&quot;
    if any(i.is_dirty for i in bpy.data.images):
        bpy.ops.image.save_all_modified()

@persistent
def pack_dirty_images(dummy):
    &quot;&quot;&quot; Pack all modified images &quot;&quot;&quot; 
    for i in bpy.data.images:
        if i.is_dirty:
            i.pack()
            print(&quot;Packed:&quot;, i.name)
            
bpy.app.handlers.save_pre.append(save_mod_images)
#bpy.app.handlers.save_pre.append(pack_dirty_images)

Copy-paste the script into the text editor and run it using the play button, this is going to add the handler to the current Blender session. If you'd like to pack all unsaved images when saving the file, just un-comment bpy.app.handlers.save_pre.append(pack_dirty_images).

image-data-linter.py
For simplicity, this can be easily turned into an Add-on. Add the usual bl_info dict, register and unregister functions and make sure to remove the handler when the add-on gets un-registered, further reading: How to create a custom UI?

Added both options to the Add-on preferences so they can be easily turned on or off.
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

bl_info = {
    &quot;name&quot;: &quot;Image Data Linter&quot;,
    &quot;description&quot;: &quot;Save or pack all modified images when saving the file&quot;,
    &quot;author&quot;: &quot;brockmann&quot;,
    &quot;version&quot;: (0, 0, 1),
    &quot;blender&quot;: (2, 80, 0),
    &quot;location&quot;: &quot;&quot;,
    &quot;category&quot;: &quot;Development&quot;
}

import bpy
from bpy.app.handlers import persistent


class ImageDataLinterPrefs(bpy.types.AddonPreferences):

    bl_idname = __name__

    save: bpy.props.BoolProperty(
        name=&quot;Save modified Images&quot;,
        description=&quot;Save modified images when saving the blend&quot;,
        default=True)

    pack: bpy.props.BoolProperty(
        name=&quot;Pack un-saved Images&quot;,
        description=&quot;Pack un-saved images when saving the blend&quot;,
        default=False)

    def draw(self, context):
        layout = self.layout
        layout.prop(self, &quot;save&quot;, toggle=True, icon=&quot;DISK_DRIVE&quot;)
        layout.prop(self, &quot;pack&quot;, toggle=True, icon=&quot;PACKAGE&quot;)
        layout.separator()


@persistent
def pack_dirty_images(dummy):
    if bpy.context.preferences.addons[__name__].preferences.pack:
        for i in bpy.data.images:
            if i.is_dirty:
                i.pack()
                print(&quot;Packed:&quot;, i.name)

@persistent
def save_mod_images(dummy):
    if bpy.context.preferences.addons[__name__].preferences.save:
        &quot;&quot;&quot; Save all modified images &quot;&quot;&quot;
        if any(i.is_dirty for i in bpy.data.images):
            bpy.ops.image.save_all_modified()


def register():
    bpy.utils.register_class(ImageDataLinterPrefs)
    bpy.app.handlers.save_pre.append(save_mod_images)
    bpy.app.handlers.save_pre.append(pack_dirty_images)

def unregister():
    bpy.app.handlers.save_pre.remove(pack_dirty_images)
    bpy.app.handlers.save_pre.remove(save_mod_images)
    bpy.utils.unregister_class(ImageDataLinterPrefs)

if __name__ == &quot;__main__&quot;:
    register()

Save the script as image-data-linter.py, go to the User Preferences &gt; Add-ons &gt; Install. Further reading: How to install an Add-on