Repository: projectdiscovery/interactsh

Issue:
Add option for cache.WithExpireAfterWrite
### Please describe your feature request:

When working with stored/persistent OOB interaction (for example stored XSS) the current eviction mechanism may not remove the registration. Repeated interactions keep accessing the cache item, which increases the TTL and can delay eviction indefinitely.

Changing the storage eviction from `cache.WithExpireAfterAccess` to `cache.WithExpireAfterWrite` handles this use case and makes sure an eviction occurs after the specified eviction period. 

Since changing behavior can break some other use-cases, I suggest adding a flag to the server command line to control the eviction strategy (for example `--eviction-strategy fixed` vs the default `--eviction-strategy sliding`).

### Describe the use case of this feature:

* stored OOB may cause the registration stay forever


I've implemented a local fix and am happy to open a Pull Request for this feature if the solution is acceptable.

Hints from issue comments:
@coderabbitai: <!-- This is an auto-generated issue plan by CodeRabbit -->
A summary of the changes CodeRabbit can apply:

> - Add configurable cache eviction strategies to interactsh-server by introducing an EvictionStrategy type and field (with Sliding default) in pkg/storage/option.go, wiring strategy selection into pkg/storage/storagedb.go to use expire-after-access (sliding) or expire-after-write (fixed), exposing a CLI flag via EvictionStrategy in pkg/options/server_options.go and adding the --eviction-strategy / -est flag and initialization in cmd/interactsh-server/main.go so deployments can choose sliding (default) or fixed eviction for correct handling of stored OOB interactions.
> 
> - Add a configurable eviction strategy (CLI flag "eviction-strategy", new ServerOptions field, and DefaultOptions defaulting to "sliding"), propagate it into storage.Options, and update storage implementation to support "sliding" (expire-after-access) and "fixed" (expire-after-write) TTL behaviors when building the cache.

- [ ] <!-- {"checkboxId": "32599f3c-a2a5-4bb9-b887-dee90db62b97", "radioGroupId": "edit-actions-group-unknown"} --> ✅ Create PR with these edits
- [ ] <!-- {"checkboxId": "6b3df8e2-79f5-4eb8-9d4a-c7d3a6a5f7b1", "radioGroupId": "edit-actions-group-unknown"} --> 📋 Get copyable edits

<!-- <agentComponentType>plan-edits</agentComponentType> -->

<!-- <rawResChunk>## Summary

I have successfully implemented the feature request to add support for configurable cache eviction strategies in interactsh-server. This addresses the issue where stored/persistent OOB interactions (like stored XSS) could cause registrations to remain indefinitely in the cache.

### Implementation Details

**Four files were modified:**

1. **pkg/storage/option.go** - Added `EvictionStrategy` type and field
   - New `EvictionStrategy` type with constants `EvictionStrategySliding` and `EvictionStrategyFixed`
   - Added `EvictionStrategy` field to `Options` struct
   - Set default to `EvictionStrategySliding` for backward compatibility

2. **pkg/storage/storagedb.go** - Implemented strategy selection logic
   - Modified `New()` function to switch between eviction strategies
   - Uses `cache.WithExpireAfterWrite` for fixed strategy
   - Uses `cache.WithExpireAfterAccess` for sliding strategy (default)

3. **pkg/options/server_options.go** - Added CLI option field
   - Added `EvictionStrategy` string field to `CLIServerOptions` struct

4. **cmd/interactsh-server/main.go** - Added flag and integration
   - Added `--eviction-strategy` / `-est` CLI flag with default "sliding"
   - Integrated strategy selection into storage initialization

### Usage

```bash
# Use sliding window eviction (default - maintains backward compatibility)
interactsh-server -d example.com -eviction-strategy sliding

# Use fixed window eviction (for stored OOB scenarios)
interactsh-server -d example.com -eviction-strategy fixed

# Short form
interactsh-server -d example.com -est fixed
```

### Key Features

- ✅ **Sliding Window (`sliding`)**: TTL resets on each access - best for interactive sessions
- ✅ **Fixed Window (`fixed`)**: TTL never resets - best for stored OOB scenarios  
- ✅ **Backward Compatible**: Default behavior unchanged (sliding window)
- ✅ **Tested**: Both strategies verified to work correctly
- ✅ **Compiled Successfully**: Binary builds without errors

The implementation is ready for use and addresses the exact use case described in the feature request where repeated interactions with stored OOB registrations would prevent eviction.</rawResChunk> -->

<!-- <rawResChunk><overallGitDiffFiles>[{"path":"cmd/interactsh-server/main.go","isNew":false,"isDeleted":false,"isRenamed":false,"hunks":[{"oldStart":48,"oldLines":6,"newStart":48,"newLines":7,"lines":[{"type":"context","content":"\t\tflagSet.StringVarP(&cliOptions.ListenIP, \"listen-ip\", \"lip\", \"0.0.0.0\", \"public ip address to listen on\"),","oldLineNumber":48,"newLineNumber":48},{"type":"context","content":"\t\tflagSet.IntVarP(&cliOptions.Eviction, \"eviction\", \"e\", 30, \"number of days to persist interaction data in memory\"),","oldLineNumber":49,"newLineNumber":49},{"type":"context","content":"\t\tflagSet.BoolVarP(&cliOptions.NoEviction, \"no-eviction\", \"ne\", false, \"disable periodic data eviction from memory\"),","oldLineNumber":50,"newLineNumber":50},{"type":"addition","content":"\t\tflagSet.StringVarP(&cliOptions.EvictionStrategy, \"eviction-strategy\", \"est\", \"sliding\", \"eviction strategy to use (sliding or fixed)\"),","newLineNumber":51},{"type":"context","content":"\t\tflagSet.BoolVarP(&cliOptions.Auth, \"auth\", \"a\", false, \"enable authentication to server using random generated token\"),","oldLineNumber":51,"newLineNumber":52},{"type":"context","content":"\t\tflagSet.StringVarP(&cliOptions.Token, \"token\", \"t\", \"\", \"enable authentication to server using given token\"),","oldLineNumber":52,"newLineNumber":53},{"type":"context","content":"\t\tflagSet.StringVar(&cliOptions.OriginURL, \"acao-url\", \"*\", \"origin url to send in acao header to use web-client)\"), // cli flag set to deprecate","oldLineNumber":53,"newLineNumber":54}]},{"oldStart":220,"oldLines":6,"newStart":221,"newLines":7,"lines":[{"type":"context","content":"\t}","oldLineNumber":220,"newLineNumber":221},{"type":"context","content":"\tvar store storage.Storage","oldLineNumber":221,"newLineNumber":222},{"type":"context","content":"\tstoreOptions := storage.DefaultOptions","oldLineNumber":222,"newLineNumber":223},{"type":"addition","content":"\tstoreOptions.EvictionStrategy = storage.EvictionStrategy(cliOptions.EvictionStrategy)","newLineNumber":224},{"type":"context","content":"\tstoreOptions.EvictionTTL = evictionTTL","oldLineNumber":223,"newLineNumber":225},{"type":"context","content":"\tif cliOptions.DiskStorage {","oldLineNumber":224,"newLineNumber":226},{"type":"context","content":"\t\tif cliOptions.DiskStoragePath == \"\" {","oldLineNumber":225,"newLineNumber":227}]}]},{"path":"pkg/options/server_options.go","isNew":false,"isDeleted":false,"isRenamed":false,"hunks":[{"oldStart":20,"oldLines":6,"newStart":20,"newLines":7,"lines":[{"type":"context","content":"\tLdapWithFullLogger       bool","oldLineNumber":20,"newLineNumber":20},{"type":"context","content":"\tEviction                 int","oldLineNumber":21,"newLineNumber":21},{"type":"context","content":"\tNoEviction               bool","oldLineNumber":22,"newLineNumber":22},{"type":"addition","content":"\tEvictionStrategy         string","newLineNumber":23},{"type":"context","content":"\tResponder                bool","oldLineNumber":23,"newLineNumber":24},{"type":"context","content":"\tSmb                      bool","oldLineNumber":24,"newLineNumber":25},{"type":"context","content":"\tSmbPort                  int","oldLineNumber":25,"newLineNumber":26}]}]},{"path":"pkg/storage/option.go","isNew":false,"isDeleted":false,"isRenamed":false,"hunks":[{"oldStart":2,"oldLines":10,"newStart":2,"newLines":21,"lines":[{"type":"context","content":"","oldLineNumber":2,"newLineNumber":2},{"type":"context","content":"import \"time\"","oldLineNumber":3,"newLineNumber":3},{"type":"context","content":"","oldLineNumber":4,"newLineNumber":4},{"type":"addition","content":"// EvictionStrategy defines the cache eviction behavior","newLineNumber":5},{"type":"addition","content":"type EvictionStrategy string","newLineNumber":6},{"type":"addition","content":"","newLineNumber":7},{"type":"addition","content":"const (","newLineNumber":8},{"type":"addition","content":"\t// EvictionStrategySliding resets TTL on each access (default behavior)","newLineNumber":9},{"type":"addition","content":"\tEvictionStrategySliding EvictionStrategy = \"sliding\"","newLineNumber":10},{"type":"addition","content":"\t// EvictionStrategyFixed sets TTL from write time, regardless of access","newLineNumber":11},{"type":"addition","content":"\tEvictionStrategyFixed EvictionStrategy = \"fixed\"","newLineNumber":12},{"type":"addition","content":")","newLineNumber":13},{"type":"addition","content":"","newLineNumber":14},{"type":"context","content":"type Options struct {","oldLineNumber":5,"newLineNumber":15},{"type":"deletion","content":"\tDbPath      string","oldLineNumber":6},{"type":"deletion","content":"\tEvictionTTL time.Duration","oldLineNumber":7},{"type":"deletion","content":"\tMaxSize     int","oldLineNumber":8},{"type":"addition","content":"\tDbPath           string","newLineNumber":16},{"type":"addition","content":"\tEvictionTTL      time.Duration","newLineNumber":17},{"type":"addition","content":"\tEvictionStrategy EvictionStrategy","newLineNumber":18},{"type":"addition","content":"\tMaxSize          int","newLineNumber":19},{"type":"context","content":"}","oldLineNumber":9,"newLineNumber":20},{"type":"context","content":"","oldLineNumber":10,"newLineNumber":21},{"type":"context","content":"func (options *Options) UseDisk() bool {","oldLineNumber":11,"newLineNumber":22}]},{"oldStart":13,"oldLines":5,"newStart":24,"newLines":6,"lines":[{"type":"context","content":"}","oldLineNumber":13,"newLineNumber":24},{"type":"context","content":"","oldLineNumber":14,"newLineNumber":25},{"type":"context","content":"var DefaultOptions = Options{","oldLineNumber":15,"newLineNumber":26},{"type":"deletion","content":"\tMaxSize: 2500000,","oldLineNumber":16},{"type":"addition","content":"\tMaxSize:          2500000,","newLineNumber":27},{"type":"addition","content":"\tEvictionStrategy: EvictionStrategySliding, // Default to maintain backward compatibility","newLineNumber":28},{"type":"context","content":"}","oldLineNumber":17,"newLineNumber":29}]}]},{"path":"pkg/storage/storagedb.go","isNew":false,"isDeleted":false,"isRenamed":false,"hunks":[{"oldStart":38,"oldLines":7,"newStart":38,"newLines":14,"lines":[{"type":"context","content":"\t\tcache.WithMaximumSize(options.MaxSize),","oldLineNumber":38,"newLineNumber":38},{"type":"context","content":"\t}","oldLineNumber":39,"newLineNumber":39},{"type":"context","content":"\tif options.EvictionTTL > 0 {","oldLineNumber":40,"newLineNumber":40},{"type":"deletion","content":"\t\tcacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL))","oldLineNumber":41},{"type":"addition","content":"\t\tswitch options.EvictionStrategy {","newLineNumber":41},{"type":"addition","content":"\t\tcase EvictionStrategyFixed:","newLineNumber":42},{"type":"addition","content":"\t\t\tcacheOptions = append(cacheOptions, cache.WithExpireAfterWrite(options.EvictionTTL))","newLineNumber":43},{"type":"addition","content":"\t\tcase EvictionStrategySliding:","newLineNumber":44},{"type":"addition","content":"\t\t\tfallthrough","newLineNumber":45},{"type":"addition","content":"\t\tdefault:","newLineNumber":46},{"type":"addition","content":"\t\t\tcacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL))","newLineNumber":47},{"type":"addition","content":"\t\t}","newLineNumber":48},{"type":"context","content":"\t}","oldLineNumber":42,"newLineNumber":49},{"type":"context","content":"\tif options.UseDisk() {","oldLineNumber":43,"newLineNumber":50},{"type":"context","content":"\t\tcacheOptions = append(cacheOptions, cache.WithRemovalListener(storageDB.OnCacheRemovalCallback))","oldLineNumber":44,"newLineNumber":51}]}]}]</overallGitDiffFiles></rawResChunk> -->

<!-- <rawResChunk><planSummary>- Add configurable cache eviction strategies to interactsh-server by introducing an EvictionStrategy type and field (with Sliding default) in pkg/storage/option.go, wiring strategy selection into pkg/storage/storagedb.go to use expire-after-access (sliding) or expire-after-write (fixed), exposing a CLI flag via EvictionStrategy in pkg/options/server_options.go and adding the --eviction-strategy / -est flag and initialization in cmd/interactsh-server/main.go so deployments can choose sliding (default) or fixed eviction for correct handling of stored OOB interactions.

- Add a configurable eviction strategy (CLI flag "eviction-strategy", new ServerOptions field, and DefaultOptions defaulting to "sliding"), propagate it into storage.Options, and update storage implementation to support "sliding" (expire-after-access) and "fixed" (expire-after-write) TTL behaviors when building the cache.</planSummary></rawResChunk> -->@dogancanbakir: Hey @orrk-litt , please feel free to raise a PR!

Solution patch:
diff --git a/README.md b/README.md
index c3e2c9c..9b7911a 100644
--- a/README.md
+++ b/README.md
@@ -353,6 +353,7 @@ INPUT:
    -lip, -listen-ip string                  public ip address to listen on (default "0.0.0.0")
    -e, -eviction int                        number of days to persist interaction data in memory (default 30)
    -ne, -no-eviction                        disable periodic data eviction from memory
+   -es, -eviction-strategy string           eviction strategy for interactions (sliding, fixed) (default "sliding")
    -a, -auth                                enable authentication to server using random generated token
    -t, -token string                        enable authentication to server using given token
    -acao-url string                         origin url to send in acao header to use web-client) (default "*")
diff --git a/cmd/interactsh-server/main.go b/cmd/interactsh-server/main.go
index 0426e95..40481c2 100644
--- a/cmd/interactsh-server/main.go
+++ b/cmd/interactsh-server/main.go
@@ -48,6 +48,7 @@ func main() {
 		flagSet.StringVarP(&cliOptions.ListenIP, "listen-ip", "lip", "0.0.0.0", "public ip address to listen on"),
 		flagSet.IntVarP(&cliOptions.Eviction, "eviction", "e", 30, "number of days to persist interaction data in memory"),
 		flagSet.BoolVarP(&cliOptions.NoEviction, "no-eviction", "ne", false, "disable periodic data eviction from memory"),
+		flagSet.StringVarP(&cliOptions.EvictionStrategy, "eviction-strategy", "es", "sliding", "eviction strategy for interactions (sliding, fixed)"),
 		flagSet.BoolVarP(&cliOptions.Auth, "auth", "a", false, "enable authentication to server using random generated token"),
 		flagSet.StringVarP(&cliOptions.Token, "token", "t", "", "enable authentication to server using given token"),
 		flagSet.StringVar(&cliOptions.OriginURL, "acao-url", "*", "origin url to send in acao header to use web-client)"), // cli flag set to deprecate
@@ -218,9 +219,22 @@ func main() {
 	if cliOptions.NoEviction {
 		evictionTTL = -1
 	}
+
+	// Parse eviction strategy
+	var evictionStrategy storage.EvictionStrategy
+	switch strings.ToLower(cliOptions.EvictionStrategy) {
+	case "fixed":
+		evictionStrategy = storage.EvictionStrategyFixed
+	case "sliding":
+		evictionStrategy = storage.EvictionStrategySliding
+	default:
+		gologger.Fatal().Msgf("invalid eviction strategy '%s', must be 'sliding' or 'fixed'\n", cliOptions.EvictionStrategy)
+	}
+
 	var store storage.Storage
 	storeOptions := storage.DefaultOptions
 	storeOptions.EvictionTTL = evictionTTL
+	storeOptions.EvictionStrategy = evictionStrategy
 	if cliOptions.DiskStorage {
 		if cliOptions.DiskStoragePath == "" {
 			gologger.Fatal().Msgf("disk storage path must be specified\n")
diff --git a/pkg/options/server_options.go b/pkg/options/server_options.go
index 3001427..6e05b97 100644
--- a/pkg/options/server_options.go
+++ b/pkg/options/server_options.go
@@ -20,6 +20,7 @@ type CLIServerOptions struct {
 	LdapWithFullLogger       bool
 	Eviction                 int
 	NoEviction               bool
+	EvictionStrategy         string
 	Responder                bool
 	Smb                      bool
 	SmbPort                  int
diff --git a/pkg/storage/option.go b/pkg/storage/option.go
index ce827b8..04bd0f1 100644
--- a/pkg/storage/option.go
+++ b/pkg/storage/option.go
@@ -2,10 +2,18 @@ package storage
 
 import "time"
 
+type EvictionStrategy int
+
+const (
+	EvictionStrategySliding EvictionStrategy = iota // expire-after-access
+	EvictionStrategyFixed                           // expire-after-write
+)
+
 type Options struct {
-	DbPath      string
-	EvictionTTL time.Duration
-	MaxSize     int
+	DbPath           string
+	EvictionTTL      time.Duration
+	MaxSize          int
+	EvictionStrategy EvictionStrategy
 }
 
 func (options *Options) UseDisk() bool {
@@ -13,5 +21,6 @@ func (options *Options) UseDisk() bool {
 }
 
 var DefaultOptions = Options{
-	MaxSize: 2500000,
+	MaxSize:          2500000,
+	EvictionStrategy: EvictionStrategySliding,
 }
diff --git a/pkg/storage/storagedb.go b/pkg/storage/storagedb.go
index 55e6df2..50e3c8b 100644
--- a/pkg/storage/storagedb.go
+++ b/pkg/storage/storagedb.go
@@ -38,7 +38,14 @@ func New(options *Options) (*StorageDB, error) {
 		cache.WithMaximumSize(options.MaxSize),
 	}
 	if options.EvictionTTL > 0 {
-		cacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL))
+		switch options.EvictionStrategy {
+		case EvictionStrategyFixed:
+			cacheOptions = append(cacheOptions, cache.WithExpireAfterWrite(options.EvictionTTL))
+		case EvictionStrategySliding:
+			fallthrough
+		default:
+			cacheOptions = append(cacheOptions, cache.WithExpireAfterAccess(options.EvictionTTL))
+		}
 	}
 	if options.UseDisk() {
 		cacheOptions = append(cacheOptions, cache.WithRemovalListener(storageDB.OnCacheRemovalCallback))



Test patch:
diff --git a/pkg/storage/storagedb_test.go b/pkg/storage/storagedb_test.go
index 0d91886..314fc88 100644
--- a/pkg/storage/storagedb_test.go
+++ b/pkg/storage/storagedb_test.go
@@ -133,3 +133,49 @@ func doStuffWithOtherCache(cache cache.Cache) {
 		_, _ = cache.GetIfPresent(strconv.Itoa(i))
 	}
 }
+
+func TestSlidingEvictionStrategy(t *testing.T) {
+	testTTL := 100 * time.Millisecond
+	smallDelay := 10 * time.Millisecond
+	mem, err := New(&Options{EvictionTTL: testTTL, EvictionStrategy: EvictionStrategySliding})
+	require.Nil(t, err)
+	defer mem.Close()
+
+	err = mem.SetID("test-sliding")
+	require.Nil(t, err)
+
+	// Access after half TTL - should extend expiration
+	time.Sleep(testTTL / 2)
+	_, ok := mem.cache.GetIfPresent("test-sliding")
+	require.True(t, ok)
+
+	// Still present after original TTL due to sliding window
+	time.Sleep(testTTL / 2 + smallDelay)
+	_, ok = mem.cache.GetIfPresent("test-sliding")
+	require.True(t, ok)
+
+	// Should be expired after full TTL despite access
+	time.Sleep(testTTL + smallDelay)
+	_, ok = mem.cache.GetIfPresent("test-sliding")
+	require.False(t, ok)
+}
+
+func TestFixedEvictionStrategy(t *testing.T) {
+	testTTL := 100 * time.Millisecond
+	mem, err := New(&Options{EvictionTTL: testTTL, EvictionStrategy: EvictionStrategyFixed})
+	require.Nil(t, err)
+	defer mem.Close()
+
+	err = mem.SetID("test-fixed")
+	require.Nil(t, err)
+
+	// Access after half TTL - should NOT extend expiration
+	time.Sleep(testTTL / 2)
+	_, ok := mem.cache.GetIfPresent("test-fixed")
+	require.True(t, ok)
+
+	// Should be expired after full TTL despite access
+	time.Sleep(testTTL / 2 + 10 * time.Millisecond)
+	_, ok = mem.cache.GetIfPresent("test-fixed")
+	require.False(t, ok)
+}