Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: "ERDDAP Custom CodeQL Configuration"

packs:
java-kotlin:
- ./.github/codeql/extensions
7 changes: 7 additions & 0 deletions .github/codeql/extensions/codeql-pack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: erddap/vulnerability-barriers
version: 0.0.1
library: true
extensionTargets:
codeql/java-all: '*'
dataExtensions:
- erddap-models.yml
35 changes: 35 additions & 0 deletions .github/codeql/extensions/erddap-models.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
extensions:
- addsTo:
pack: codeql/java-all
extensible: barrierModel
data:
# ========================================================================
# 1. LEGACY INITIALIZATION PATHWAY
# ========================================================================
- ["gov.noaa.pfel.erddap.dataset", "EDD", true, "fromXml", "", "", "ReturnValue", "path-injection", "manual"]
- ["gov.noaa.pfel.erddap.dataset", "EDD", true, "fromXml", "", "", "ReturnValue", "sql-injection", "manual"]
- ["gov.noaa.pfel.erddap.dataset", "EDD", true, "fromXml", "", "", "ReturnValue", "command-injection", "manual"]

- ["gov.noaa.pfel.erddap", "LoadDatasets", false, "parseUsingSimpleXmlReader", "", "", "Argument[0]", "path-injection", "manual"]
- ["gov.noaa.pfel.erddap", "LoadDatasets", false, "parseUsingSimpleXmlReader", "", "", "Argument[0]", "sql-injection", "manual"]

# ========================================================================
# 2. MODERN INITIALIZATION PATHWAY (Concrete Handlers Only)
# ========================================================================
- ["gov.noaa.pfel.erddap", "DatasetHandler", true, "characters", "", "", "Argument[0]", "path-injection", "manual"]
- ["gov.noaa.pfel.erddap", "DatasetHandler", true, "characters", "", "", "Argument[0]", "sql-injection", "manual"]
- ["gov.noaa.pfel.erddap", "DatasetHandler", true, "startElement", "", "", "Argument[3]", "path-injection", "manual"]

- ["gov.noaa.pfel.erddap", "TopLevelHandler", true, "characters", "", "", "Argument[0]", "path-injection", "manual"]
- ["gov.noaa.pfel.erddap", "TopLevelHandler", true, "characters", "", "", "Argument[0]", "sql-injection", "manual"]

# ========================================================================
# 3. EDCONFIG PATHWAY (Trusted Configuration Container)
# ========================================================================
# Leaving the method name blank ("") acts as a wildcard. CodeQL will
# automatically consider any data returning from ANY getter or method
# inside EDConfig as entirely safe and untainted.
- ["gov.noaa.pfel.erddap.util", "EDConfig", false, "", "", "", "ReturnValue", "path-injection", "manual"]
- ["gov.noaa.pfel.erddap.util", "EDConfig", false, "", "", "", "ReturnValue", "sql-injection", "manual"]
- ["gov.noaa.pfel.erddap.util", "EDConfig", false, "", "", "", "ReturnValue", "command-injection", "manual"]
- ["gov.noaa.pfel.erddap.util", "EDConfig", false, "", "", "", "ReturnValue", "request-ssrf", "manual"]
63 changes: 63 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: "CodeQL Advanced"

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '35 15 * * 2'

jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read
actions: read
contents: read

strategy:
fail-fast: false
matrix:
include:
# Keep actions to monitor your workflow files for security flaws
- language: actions
build-mode: none
# Switch Java to manual build mode for maximum taint-tracking accuracy
- language: java-kotlin
build-mode: manual

steps:
- name: Checkout repository
uses: actions/checkout@v4

# Set up the Java runtime specifically for the manual compilation step
Comment on lines +33 to +36
- name: Set up JDK
if: matrix.build-mode == 'manual'
uses: actions/setup-java@v4
with:
java-version: '25' # Adjust to match ERDDAP's required target JDK
distribution: 'temurin'
cache: 'maven'

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yml

# Intercept the Java compilation process to map the Abstract Syntax Tree
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
mvn clean compile -DskipTests

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
182 changes: 182 additions & 0 deletions WEB-INF/classes/com/cohort/util/LinkHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package com.cohort.util;

import java.util.EnumSet;
import org.nibor.autolink.LinkExtractor;
import org.nibor.autolink.LinkSpan;
import org.nibor.autolink.LinkType;

/** A helper class for finding and splitting links in text using autolink-java. */
public class LinkHelper {

private static final LinkExtractor EXTRACTOR =
LinkExtractor.builder().linkTypes(EnumSet.of(LinkType.URL, LinkType.WWW)).build();

/**
* A CharSequence that lazily replaces backslashes with forward slashes and masks spaces and
* quotes within URLs to allow autolink-java to recognize URLs with quoted spaces.
*/
private static class NormalizedCharSequence implements CharSequence {
private final String source;
private final char[] masked;

NormalizedCharSequence(String source) {
this.source = source;
this.masked = source.toCharArray();
boolean inDoubleQuote = false;
for (int i = 0; i < masked.length; i++) {
char c = masked[i];

if (c == '\"') {
inDoubleQuote = !inDoubleQuote;
masked[i] = 'z'; // Mask quote as alphanumeric to ensure linker includes it
continue;
}

if (c == '%' && i + 2 < masked.length && masked[i + 1] == '2' && masked[i + 2] == '2') {
inDoubleQuote = !inDoubleQuote;
// Mask %22 as zzz to ensure linker includes it and toggles quote state
masked[i] = 'z';
masked[i + 1] = 'z';
masked[i + 2] = 'z';
i += 2;
continue;
}

if (c == '\\') {
masked[i] = '/';
} else if (c == ' ' && inDoubleQuote) {
masked[i] = '_';
}
}
}

@Override
public int length() {
return source.length();
}

@Override
public char charAt(int index) {
return masked[index];
}

@Override
public CharSequence subSequence(int start, int end) {
return new NormalizedCharSequence(source.substring(start, end));
}

@Override
public String toString() {
return new String(masked);
}
}

private static boolean isValidLink(CharSequence searchIn, LinkSpan span) {
if (span.getType() == LinkType.WWW) {
return true;
}
// LinkType.URL
int begin = span.getBeginIndex();
int end = span.getEndIndex();

// Work on normalized text for validation
String linkText = searchIn.subSequence(begin, end).toString().toLowerCase();

// Find "://" (searchIn is already normalized, so backslashes are forward slashes)
int schemeEnd = linkText.indexOf("://");
if (schemeEnd == -1) return false;

// Check if it's file:
if (schemeEnd == 4 && linkText.startsWith("file")) {
return true;
}

int hostStart = schemeEnd + 3;
// Skip extra slashes (e.g. http:////)
while (hostStart < linkText.length() && linkText.charAt(hostStart) == '/') {
hostStart++;
}
if (hostStart >= linkText.length()) return false;

// Check for user:pass@
int atSign = -1;
for (int i = hostStart; i < linkText.length(); i++) {
char c = linkText.charAt(i);
if (c == '/' || c == '?' || c == '#') break;
if (c == '@') {
atSign = i;
break;
}
}
if (atSign != -1) {
hostStart = atSign + 1;
}
if (hostStart >= linkText.length()) return false;

// Check for localhost
if (linkText.substring(hostStart).startsWith("localhost")) {
int endOfLocalhost = hostStart + 9;
if (endOfLocalhost == linkText.length()) return true;
char c = linkText.charAt(endOfLocalhost);
if (c == '/' || c == ':' || c == '?' || c == '#') return true;
}

// Find host end and check for dot
boolean hasDot = false;
for (int i = hostStart; i < linkText.length(); i++) {
char c = linkText.charAt(i);
if (c == '/' || c == ':' || c == '?' || c == '#') break;
if (c == '.') hasDot = true;
}
return hasDot;
}

/** Functional interface for handling parts of a string during linkification. */
@FunctionalInterface
public interface LinkPartHandler {
void handle(String text, boolean isUrl);
}

/**
* Processes the input string, identifying links and plain text segments, and passes each to the
* handler.
*
* @param input the text to process
* @param handler the handler to receive text and URL segments
* @return true if at least one link was found and handled
*/
public static boolean linkify(String input, LinkPartHandler handler) {
if (input == null || input.isEmpty()) {
return false;
}

CharSequence searchIn = new NormalizedCharSequence(input);
int lastEnd = 0;
boolean found = false;
for (LinkSpan span : EXTRACTOR.extractLinks(searchIn)) {
if (isValidLink(searchIn, span)) {
found = true;
if (span.getBeginIndex() > lastEnd) {
handler.handle(input.substring(lastEnd, span.getBeginIndex()), false);
}
handler.handle(input.substring(span.getBeginIndex(), span.getEndIndex()), true);
lastEnd = span.getEndIndex();
}
}
if (lastEnd < input.length()) {
handler.handle(input.substring(lastEnd), false);
}
return found;
}

/**
* This is used when setting href attributes in anchor tags. Specifically this is to make sure
* browsers know this is an absolute url and not a relative url.
*/
public static String addHttpsForWWW(final String input) {
if (input != null && input.startsWith("www.")) {
return "https://" + input;
}
return input;
}
}
Loading
Loading